stable-cli-rails 0.7.11 → 0.8.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/stable/bootstrap.rb +1 -0
  3. data/lib/stable/cli.rb +2 -5
  4. data/lib/stable/commands/doctor.rb +1 -0
  5. data/lib/stable/commands/list.rb +7 -6
  6. data/lib/stable/commands/new.rb +1 -0
  7. data/lib/stable/commands/remove.rb +1 -0
  8. data/lib/stable/commands/restart.rb +1 -0
  9. data/lib/stable/commands/setup.rb +1 -0
  10. data/lib/stable/commands/start.rb +1 -0
  11. data/lib/stable/commands/stop.rb +1 -0
  12. data/lib/stable/db_manager.rb +22 -7
  13. data/lib/stable/paths.rb +10 -1
  14. data/lib/stable/registry.rb +57 -7
  15. data/lib/stable/scanner.rb +1 -0
  16. data/lib/stable/services/app_creator.rb +118 -61
  17. data/lib/stable/services/app_registry.rb +7 -19
  18. data/lib/stable/services/app_remover.rb +1 -0
  19. data/lib/stable/services/app_restarter.rb +1 -0
  20. data/lib/stable/services/app_starter.rb +1 -0
  21. data/lib/stable/services/app_stopper.rb +1 -0
  22. data/lib/stable/services/caddy_manager.rb +1 -0
  23. data/lib/stable/services/database/base.rb +12 -2
  24. data/lib/stable/services/database/mysql.rb +1 -0
  25. data/lib/stable/services/database/postgres.rb +1 -0
  26. data/lib/stable/services/dependency_checker.rb +58 -9
  27. data/lib/stable/services/hosts_manager.rb +18 -7
  28. data/lib/stable/services/process_manager.rb +1 -0
  29. data/lib/stable/services/ruby.rb +10 -0
  30. data/lib/stable/services/setup_runner.rb +110 -16
  31. data/lib/stable/system/shell.rb +1 -0
  32. data/lib/stable/utils/package_manager.rb +68 -0
  33. data/lib/stable/utils/platform.rb +76 -0
  34. data/lib/stable/utils/prompts.rb +1 -0
  35. data/lib/stable/validators/app_name.rb +1 -0
  36. data/lib/stable.rb +1 -0
  37. metadata +6 -31
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe9114ae5eae07811ec911b967cef33620ebf62c7df4f30c202f3743f7d6bea5
4
- data.tar.gz: 91920b483e2c26e58d1ef7ca7c786ea5cf91a4849ac16ec276fee4cdbf72d878
3
+ metadata.gz: ab8c647151029030baf0e96737f45f20cc6b067b952b032b98eac46e5e14e158
4
+ data.tar.gz: 5db558cabc869e1f6d6684d26b1d7b08178679194d09ebdbbd42aa67d828aee7
5
5
  SHA512:
6
- metadata.gz: dbdc4b736300bd5a395f67b9138e3aa7410a0184d4f5ea2be6f5e3445934fcf57588b526e5b957ee0c551247ab3013febd29eaffd96cc7bde43f982df91e3b53
7
- data.tar.gz: 19e8dd338cd34dd6621159785171c970468d43b0f423048c4c3fc10bd4ab4851cedd59091a03e1748acf990fa1853a7b1da33bbadba552bae186a71a69799f3e
6
+ metadata.gz: fe68968ac73be6725262a224ceda502eaee576c2aa3a8822a2742577e9f5d6fc05f1c3092c6f7f1c0f011218aaa851949863e3473e8ea95de9e6fd91b94f31dc
7
+ data.tar.gz: e333ac1da62ceb50a7a77ec83d1c145900f90b8888249c4307102bc1dfb0e3661e0bf97c1efe5a33be27b1aa409e639278ca91643989c3c2c0954d763c665533
@@ -3,6 +3,7 @@
3
3
  require 'fileutils'
4
4
 
5
5
  module Stable
6
+ # Bootstrap utilities for setting up Stable environment
6
7
  module Bootstrap
7
8
  def self.run!
8
9
  FileUtils.mkdir_p(Paths.root)
data/lib/stable/cli.rb CHANGED
@@ -9,12 +9,12 @@ require_relative 'scanner'
9
9
  require_relative 'registry'
10
10
 
11
11
  module Stable
12
+ # Main CLI class for the Stable command-line interface
12
13
  class CLI < Thor
13
14
  def initialize(*)
14
15
  super
15
16
  Stable::Bootstrap.run!
16
17
  Services::SetupRunner.ensure_dependencies!
17
- dedupe_registry!
18
18
  end
19
19
 
20
20
  def self.exit_on_failure?
@@ -29,6 +29,7 @@ module Stable
29
29
  method_option :db, type: :string, desc: 'Database name to create and integrate'
30
30
  method_option :postgres, type: :boolean, default: false, desc: 'Use Postgres for the database'
31
31
  method_option :mysql, type: :boolean, default: false, desc: 'Use MySQL for the database'
32
+ method_option :full, type: :boolean, default: false, desc: 'Setup full Rails app common generators'
32
33
  def new(name, ruby: RUBY_VERSION, rails: nil, port: nil)
33
34
  safe_name = Validators::AppName.call!(name)
34
35
  Commands::New.new(safe_name, options).call
@@ -157,9 +158,5 @@ module Stable
157
158
  def port_in_use?(port)
158
159
  system("lsof -i tcp:#{port} > /dev/null 2>&1")
159
160
  end
160
-
161
- def dedupe_registry!
162
- Services::AppRegistry.dedupe
163
- end
164
161
  end
165
162
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Commands
5
+ # Doctor command - checks system health and dependencies
5
6
  class Doctor
6
7
  def call
7
8
  puts 'Running Stable health checks...'
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Commands
5
+ # List command - displays all registered applications
5
6
  class List
6
7
  def call
7
8
  apps = Services::AppRegistry.all
@@ -34,12 +35,12 @@ module Stable
34
35
  end
35
36
 
36
37
  format(
37
- '%-18s %-26s %-8s %-10s %-10s',
38
- app[:name],
39
- app[:domain],
40
- app[:port],
41
- app[:ruby],
42
- status
38
+ '%<name>-18s %<domain>-26s %<port>-8s %<ruby>-10s %<status>-10s',
39
+ name: app[:name],
40
+ domain: app[:domain],
41
+ port: app[:port],
42
+ ruby: app[:ruby],
43
+ status: status
43
44
  )
44
45
  end
45
46
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Commands
5
+ # New command - creates a new Rails application
5
6
  class New
6
7
  def initialize(name, options)
7
8
  @name = name
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Commands
5
+ # Remove command - removes a Rails application
5
6
  class Remove
6
7
  def initialize(name)
7
8
  @name = name
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Commands
5
+ # Restart command - restarts a Rails application
5
6
  class Restart
6
7
  def initialize(name)
7
8
  @name = name
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Commands
5
+ # Setup command - initializes Stable environment
5
6
  class Setup
6
7
  def call
7
8
  Services::SetupRunner.new.call
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Commands
5
+ # Start command - starts a Rails application
5
6
  class Start
6
7
  def initialize(name)
7
8
  @name = name
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Commands
5
+ # Stop command - stops a Rails application
5
6
  class Stop
6
7
  def initialize(name)
7
8
  @name = name
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stable
4
+ # Database management utilities for Rails applications
4
5
  class DBManager
5
6
  attr_reader :name, :adapter
6
7
 
@@ -101,16 +102,30 @@ module Stable
101
102
  )) or abort('Failed to repair MySQL root authentication')
102
103
  end
103
104
 
104
- # Detect MySQL socket on macOS / Linux
105
+ # Detect MySQL socket on macOS / Linux / Windows
105
106
  def mysql_socket
106
- paths = [
107
- '/opt/homebrew/var/mysql/mysql.sock', # Homebrew macOS
108
- '/tmp/mysql.sock', # Default
109
- '/var/run/mysqld/mysqld.sock' # Linux default
110
- ]
107
+ platform = RUBY_PLATFORM
108
+
109
+ paths = case platform
110
+ when /darwin/ # macOS
111
+ [
112
+ '/opt/homebrew/var/mysql/mysql.sock', # Homebrew macOS
113
+ '/tmp/mysql.sock' # Fallback
114
+ ]
115
+ when /linux/
116
+ [
117
+ '/var/run/mysqld/mysqld.sock', # Linux default
118
+ '/tmp/mysql.sock' # Fallback
119
+ ]
120
+ when /mingw|mswin|win32/ # Windows
121
+ # Windows typically uses TCP connections, not sockets
122
+ return nil
123
+ else
124
+ ['/tmp/mysql.sock'] # Generic fallback
125
+ end
111
126
 
112
127
  paths.each { |p| return p if File.exist?(p) }
113
- abort 'MySQL socket not found. Is MySQL running?'
128
+ abort 'MySQL socket not found. Is MySQL running? (On Windows, ensure MySQL is configured for TCP connections)'
114
129
  end
115
130
 
116
131
  # Default Rails DB user
data/lib/stable/paths.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stable
4
+ # Path utilities for Stable configuration and data directories
4
5
  module Paths
5
6
  def self.root
6
- File.expand_path('~/StableCaddy')
7
+ ENV['STABLE_TEST_ROOT'] || File.expand_path('~/StableCaddy')
7
8
  end
8
9
 
9
10
  def self.caddy_dir
@@ -21,5 +22,13 @@ module Stable
21
22
  def self.apps_file
22
23
  File.join(root, 'apps.yml')
23
24
  end
25
+
26
+ def self.projects_dir
27
+ File.join(root, 'projects')
28
+ end
29
+
30
+ def self.app_config_file(app_name)
31
+ File.join(projects_dir, app_name, "#{app_name}.yml")
32
+ end
24
33
  end
25
34
  end
@@ -4,17 +4,39 @@ require 'yaml'
4
4
  require 'fileutils'
5
5
 
6
6
  module Stable
7
+ # Application registry for managing Rails app configurations
7
8
  class Registry
8
- def self.file_path
9
- Stable::Paths.apps_file
10
- end
9
+ def self.apps
10
+ apps = []
11
+
12
+ # Read legacy apps.yml file for backward compatibility
13
+ legacy_file = Stable::Paths.apps_file
14
+ if File.exist?(legacy_file)
15
+ legacy_apps = load_legacy_apps(legacy_file)
16
+ apps.concat(legacy_apps)
17
+ end
18
+
19
+ # Read individual app config files from projects directory
20
+ projects_dir = Stable::Paths.projects_dir
21
+ if Dir.exist?(projects_dir)
22
+ Dir.glob(File.join(projects_dir, '*/')).each do |app_dir|
23
+ app_name = File.basename(app_dir)
24
+ config_file = Stable::Paths.app_config_file(app_name)
25
+
26
+ next unless File.exist?(config_file)
11
27
 
12
- def self.save(apps)
13
- FileUtils.mkdir_p(Stable.root)
14
- File.write(file_path, apps.to_yaml)
28
+ # Skip if we already have this app from legacy file
29
+ next if apps.any? { |app| app[:name] == app_name }
30
+
31
+ app_config = load_app_config(app_name)
32
+ apps << app_config if app_config
33
+ end
34
+ end
35
+
36
+ apps
15
37
  end
16
38
 
17
- def self.apps
39
+ def self.load_legacy_apps(file_path)
18
40
  return [] unless File.exist?(file_path)
19
41
 
20
42
  data = YAML.load_file(file_path) || []
@@ -27,5 +49,33 @@ module Stable
27
49
  end
28
50
  end
29
51
  end
52
+
53
+ def self.save_app_config(app_name, config)
54
+ config_file = Stable::Paths.app_config_file(app_name)
55
+ FileUtils.mkdir_p(File.dirname(config_file))
56
+ File.write(config_file, config.to_yaml)
57
+ end
58
+
59
+ def self.load_app_config(app_name)
60
+ config_file = Stable::Paths.app_config_file(app_name)
61
+ return nil unless File.exist?(config_file)
62
+
63
+ parse_config_file(config_file)
64
+ end
65
+
66
+ def self.remove_app_config(app_name)
67
+ config_file = Stable::Paths.app_config_file(app_name)
68
+ FileUtils.rm_f(config_file)
69
+ end
70
+
71
+ def self.parse_config_file(config_file)
72
+ data = YAML.load_file(config_file)
73
+ return nil unless data.is_a?(Hash)
74
+
75
+ data.each_with_object({}) do |(k, v), memo|
76
+ key = k.is_a?(String) ? k.to_sym : k
77
+ memo[key] = v
78
+ end
79
+ end
30
80
  end
31
81
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stable
4
+ # Scanner utility for application discovery and management
4
5
  class Scanner
5
6
  def self.run
6
7
  Registry.save([])
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Services
5
+ # Service for creating new Rails applications
5
6
  class AppCreator
6
7
  def initialize(name, options)
7
8
  @name = name
@@ -11,59 +12,58 @@ module Stable
11
12
  def call
12
13
  ruby = @options[:ruby] || RUBY_VERSION
13
14
  port = @options[:port] || next_free_port
14
- app_path = File.expand_path(@name)
15
+ app_path = File.join(Stable::Paths.projects_dir, @name)
15
16
  domain = "#{@name}.test"
16
17
 
17
- # --- Register app in registry ---
18
- Services::AppRegistry.add(name: @name, path: app_path, domain: domain, port: port, ruby: ruby, started_at: nil,
19
- pid: nil)
18
+ # --- Check if app already exists ---
19
+ config_file = Stable::Paths.app_config_file(@name)
20
+ abort "App '#{@name}' already exists" if File.exist?(config_file)
20
21
 
21
- abort "Folder already exists: #{app_path}" if File.exist?(app_path)
22
+ # --- Register app in registry ---
23
+ Services::AppRegistry.add(
24
+ name: @name, path: app_path, domain: domain, port: port,
25
+ ruby: ruby, started_at: nil, pid: nil
26
+ )
22
27
 
23
- # --- Ensure Ruby + gemset ---
28
+ # --- Ensure Ruby version & RVM ---
24
29
  Ruby.ensure_version(ruby)
25
30
  Ruby.ensure_rvm!
26
- System::Shell.run("bash -lc 'source #{Ruby.rvm_script} && rvm #{ruby}@#{@name} --create do true'")
31
+
32
+ # --- Create gemset ---
33
+ System::Shell.run("bash -lc 'source #{Ruby.rvm_script} && rvm #{ruby} do rvm gemset create #{@name} || true'")
27
34
 
28
35
  rvm_cmd = Ruby.rvm_prefix(ruby, @name)
29
36
 
30
- # --- Install Rails into gemset if missing ---
31
- rails_version = @options[:rails]
32
- rails_check_cmd = if rails_version
33
- "#{rvm_cmd} gem list -i rails -v #{rails_version}"
34
- else
35
- "#{rvm_cmd} gem list -i rails"
36
- end
37
+ # --- Install Bundler ---
38
+ System::Shell.run("bash -lc '#{rvm_cmd} gem install bundler --no-document'")
37
39
 
40
+ # --- Install Rails if missing ---
41
+ rails_version = @options[:rails]
42
+ rails_check_cmd = rails_version ? "#{rvm_cmd} gem list -i rails -v #{rails_version}" : "#{rvm_cmd} gem list -i rails"
38
43
  unless system("bash -lc '#{rails_check_cmd}'")
39
- puts "Installing Rails #{rails_version || 'latest'} in gemset..."
44
+ puts "Installing Rails #{rails_version || 'latest'}..."
40
45
  install_cmd = rails_version ? "#{rvm_cmd} gem install rails -v #{rails_version}" : "#{rvm_cmd} gem install rails"
41
- system("bash -lc '#{install_cmd}'") or abort('Failed to install Rails')
46
+ system("bash -lc '#{install_cmd}'") or abort("Failed to install Rails #{rails_version || ''}")
42
47
  end
43
48
 
44
49
  # --- Create Rails app ---
45
50
  puts "Creating Rails app #{@name} (Ruby #{ruby})..."
46
- System::Shell.run("bash -lc '#{rvm_cmd} rails new #{app_path}'")
51
+ rails_cmd = "#{rvm_cmd} rails new #{app_path} " \
52
+ '--skip-importmap --skip-hotwire --skip-javascript --skip-solid'
53
+ System::Shell.run("bash -lc '#{rails_cmd}'")
47
54
 
48
- # --- Write ruby version/gemset and bundle install inside gemset ---
55
+ # --- Write ruby version/gemset ---
49
56
  Dir.chdir(app_path) do
50
57
  File.write('.ruby-version', "#{ruby}\n")
51
58
  File.write('.ruby-gemset', "#{@name}\n")
52
-
53
- puts 'Running bundle install...'
54
- System::Shell.run("bash -lc '#{rvm_cmd} bundle install --jobs=4 --retry=3'")
55
59
  end
56
60
 
57
61
  # --- Database integration ---
58
62
  if @options[:db]
59
- adapter = if @options[:mysql]
60
- :mysql
61
- else
62
- :postgresql
63
- end
64
-
63
+ adapter = @options[:mysql] ? :mysql : :postgresql
65
64
  gem_name = adapter == :postgresql ? 'pg' : 'mysql2'
66
65
  gemfile_path = File.join(app_path, 'Gemfile')
66
+
67
67
  unless File.read(gemfile_path).include?(gem_name)
68
68
  File.open(gemfile_path, 'a') do |f|
69
69
  f.puts "\n# Added by Stable CLI"
@@ -72,17 +72,86 @@ module Stable
72
72
  puts "✅ Added '#{gem_name}' gem to Gemfile"
73
73
  end
74
74
 
75
- # ensure gem is installed inside gemset
76
- System::Shell.run("bash -lc 'cd #{app_path} && #{rvm_cmd} bundle install --jobs=4 --retry=3'")
75
+ # Use correct Ruby/gemset for bundle install
76
+ System::Shell.run(rvm_run('bundle install --jobs=4 --retry=3', chdir: app_path))
77
77
 
78
- # run adapter setup which will write database.yml and prepare
79
78
  db_adapter = adapter == :mysql ? Database::MySQL : Database::Postgres
80
- db_adapter.new(app_name: @name, app_path: app_path).setup
79
+ db_adapter.new(app_name: @name, app_path: app_path, ruby: ruby).setup
80
+ end
81
+
82
+ # --- Run bundle & DB prepare ---
83
+ System::Shell.run(rvm_run('bundle install --jobs=4 --retry=3', chdir: app_path))
84
+ System::Shell.run(rvm_run('bundle exec rails db:prepare', chdir: app_path))
85
+
86
+ rails_check = "cd #{app_path} && #{rvm_cmd} bundle exec rails runner \"puts Rails.version\""
87
+ rails_version = `bash -lc '#{rails_check}'`.strip.to_f
88
+
89
+ begin
90
+ rails_ver = Gem::Version.new(rails_version)
91
+ rescue ArgumentError
92
+ rails_ver = Gem::Version.new('0.0.0')
93
+ end
94
+
95
+ # only if Rails >= 7
96
+ if rails_ver >= Gem::Version.new('7.0.0')
97
+ # Gems to re-add
98
+ optional_gems = {
99
+ 'importmap-rails' => nil,
100
+ 'turbo-rails' => nil,
101
+ 'stimulus-rails' => nil,
102
+ 'solid_cache' => nil,
103
+ 'solid_queue' => nil,
104
+ 'solid_cable' => nil
105
+ }
106
+
107
+ gemfile = File.join(app_path, 'Gemfile')
108
+
109
+ # Add gems if not already present
110
+ optional_gems.each do |gem_name, version|
111
+ next if File.read(gemfile).match?(/^gem ['"]#{gem_name}['"]/)
112
+
113
+ File.open(gemfile, 'a') do |f|
114
+ f.puts version ? "gem '#{gem_name}', '#{version}'" : "gem '#{gem_name}'"
115
+ end
116
+ end
117
+
118
+ # Install all new gems
119
+ System::Shell.run(rvm_run('bundle install', chdir: app_path))
120
+
121
+ # Run generators for installed gems
122
+ generators = [
123
+ 'importmap:install',
124
+ 'turbo:install stimulus:install',
125
+ 'solid_cache:install solid_queue:install solid_cable:install'
126
+ ]
127
+
128
+ generators.each do |cmd|
129
+ System::Shell.run(rvm_run("bundle exec rails #{cmd}", chdir: app_path))
130
+ end
81
131
  end
82
132
 
83
- # --- Refresh bundle and prepare DB (idempotent) ---
84
- System::Shell.run("bash -lc 'cd #{app_path} && #{rvm_cmd} bundle check || #{rvm_cmd} bundle install'")
85
- System::Shell.run("bash -lc 'cd #{app_path} && #{rvm_cmd} bundle exec rails db:prepare'")
133
+ # --- Add allowed host for Rails < 7.2 ---
134
+ if @options[:rails] && @options[:rails].to_f < 7.2
135
+ env_file = File.join(app_path, 'config/environments/development.rb')
136
+
137
+ if File.exist?(env_file)
138
+ content = File.read(env_file)
139
+
140
+ # Append host config inside the Rails.application.configure block
141
+ updated_content = content.gsub(/Rails\.application\.configure do(.*?)end/mm) do |_match|
142
+ inner = Regexp.last_match(1)
143
+ # Prevent duplicate entries
144
+ unless inner.include?(domain)
145
+ inner += " # allow local .test host for this app\n config.hosts << '#{domain}'\n"
146
+ end
147
+ "Rails.application.configure do#{inner}end"
148
+ end
149
+
150
+ File.write(env_file, updated_content)
151
+ else
152
+ warn "Development environment file not found: #{env_file}"
153
+ end
154
+ end
86
155
 
87
156
  # --- Hosts, certs, caddy ---
88
157
  Services::HostsManager.add(domain)
@@ -90,50 +159,31 @@ module Stable
90
159
  Services::CaddyManager.ensure_running!
91
160
  Services::CaddyManager.reload
92
161
 
93
- # --- Start the app ---
162
+ # --- Start Rails server ---
94
163
  puts "Starting Rails server for #{@name} on port #{port}..."
95
164
  log_file = File.join(app_path, 'log', 'stable.log')
96
165
  FileUtils.mkdir_p(File.dirname(log_file))
97
166
 
98
167
  abort "Port #{port} is already in use. Choose another port." if port_in_use?(port)
99
168
 
100
- pid = spawn('bash', '-lc', "cd \"#{app_path}\" && #{rvm_cmd} bundle exec rails s -p #{port} -b 127.0.0.1",
101
- out: log_file, err: log_file)
169
+ pid = spawn(
170
+ 'bash', '-lc',
171
+ "cd \"#{app_path}\" && #{rvm_cmd} bundle exec rails s -p #{port} -b 127.0.0.1",
172
+ out: log_file, err: log_file
173
+ )
102
174
  Process.detach(pid)
103
-
104
175
  AppRegistry.update(@name, started_at: Time.now.to_i, pid: pid)
105
176
 
106
177
  sleep 1.5
107
178
  wait_for_port(port)
108
- prefix = @options[:skip_ssl] ? 'http' : 'https'
109
- display_domain = if @options[:skip_ssl]
110
- "#{domain}:#{port}"
111
- else
112
- domain
113
- end
114
179
 
180
+ prefix = @options[:skip_ssl] ? 'http' : 'https'
181
+ display_domain = @options[:skip_ssl] ? "#{domain}:#{port}" : domain
115
182
  puts "✔ #{@name} running at #{prefix}://#{display_domain}"
116
183
  end
117
184
 
118
185
  private
119
186
 
120
- def create_rails_app
121
- System::Shell.run("rails new #{@name}")
122
- end
123
-
124
- def setup_database
125
- return unless @options[:mysql] || @options[:postgres]
126
-
127
- adapter =
128
- if @options[:mysql]
129
- Database::MySQL
130
- else
131
- Database::Postgres
132
- end
133
-
134
- adapter.new(@name).setup
135
- end
136
-
137
187
  def next_free_port
138
188
  used_ports = Services::AppRegistry.all.map { |a| a[:port] }
139
189
  port = 3000
@@ -158,6 +208,13 @@ module Stable
158
208
  sleep 0.5
159
209
  end
160
210
  end
211
+
212
+ def rvm_run(cmd, chdir: nil, ruby: nil)
213
+ ruby ||= @options[:ruby] || RUBY_VERSION
214
+ gemset = @name
215
+ cd = chdir ? "cd #{chdir} && " : ''
216
+ "bash -lc '#{cd}source #{Dir.home}/.rvm/scripts/rvm && rvm #{ruby}@#{gemset} do #{cmd}'"
217
+ end
161
218
  end
162
219
  end
163
220
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Services
5
+ # Application registry management service
5
6
  class AppRegistry
6
7
  class << self
7
8
  def all
@@ -42,32 +43,19 @@ module Stable
42
43
  end
43
44
 
44
45
  def add(app)
45
- apps = Stable::Registry.apps
46
- apps.reject! { |a| a[:name] == app[:name] }
47
- apps << app
48
- Stable::Registry.save(apps)
49
- end
50
-
51
- # Remove duplicate app entries (by name) and persist the canonical list
52
- def dedupe
53
- apps = Stable::Registry.apps
54
- apps.uniq! { |a| a[:name] }
55
- Stable::Registry.save(apps)
56
- apps
46
+ Stable::Registry.save_app_config(app[:name], app)
57
47
  end
58
48
 
59
49
  def remove(name)
60
- apps = Stable::Registry.apps.reject { |a| a[:name] == name }
61
- Stable::Registry.save(apps)
50
+ Stable::Registry.remove_app_config(name)
62
51
  end
63
52
 
64
53
  def update(name, attrs)
65
- apps = Stable::Registry.apps
66
- idx = apps.index { |a| a[:name] == name }
67
- return unless idx
54
+ app = find(name)
55
+ return unless app
68
56
 
69
- apps[idx] = apps[idx].merge(attrs)
70
- Stable::Registry.save(apps)
57
+ updated_app = app.merge(attrs)
58
+ Stable::Registry.save_app_config(name, updated_app)
71
59
  end
72
60
 
73
61
  def mark_stopped(name)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Services
5
+ # Service for removing Rails applications
5
6
  class AppRemover
6
7
  def initialize(name)
7
8
  @name = name
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Services
5
+ # Service for restarting Rails applications
5
6
  class AppRestarter
6
7
  def initialize(name)
7
8
  @name = name
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Services
5
+ # Service for starting Rails applications
5
6
  class AppStarter
6
7
  def initialize(name)
7
8
  @name = name
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Services
5
+ # Service for stopping Rails applications
5
6
  class AppStopper
6
7
  def initialize(name)
7
8
  @app = AppRegistry.fetch(name)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Services
5
+ # Service for managing Caddy web server configuration
5
6
  class CaddyManager
6
7
  class << self
7
8
  def caddyfile