stable-cli-rails 0.7.10 → 0.7.12

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: 8b8d5b304fbf560e181a416e2087e912c4fe2a26cd358341a263d1b890540069
4
- data.tar.gz: 6e98c5d24b16f5669f442fab9c313a3ee74c790551a776e40e56773ed901e9d3
3
+ metadata.gz: f092ba86af858aef49a2de7704a7039e3f25accafaf9028254469e9621c754ea
4
+ data.tar.gz: 4ab71b60ae850c74508c790cf0fce5b14c2d12e0c8f2ec793a6b5e5234a95529
5
5
  SHA512:
6
- metadata.gz: 41399496362c590019b87351d9e69364b41e6a3ecf0f10a7906a1c3edbaa508557104bd33cfb855623ee0b1e75e690b4d7834a42bd90025bfc5f23bc28dd2bf3
7
- data.tar.gz: e4779489d26bd0b472a3a6f545039296bb7481aa87df1dbfa0748acd440f3e78b79f091b318956d6bc92aae8e7179f1f66393f9d800fff5b015a47bb3c2edc45
6
+ metadata.gz: 830108d826c9a63cdbaabf2c539b083795d7cf2772e1d8cea0c91d39dcefcaf06e792e1865793d7c7ab1da428b810228a36bbba34038555bcf20aefe03d2c295
7
+ data.tar.gz: acdf43e4787cf9115eb29b4900f670cb9e2ce0a0fed29ba3f251cf40f00948897a3ed1c1edff3109053031c3ed6d238e4632d28f681fc591548635f9f03951ac
data/bin/stable CHANGED
@@ -1,3 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- require "stable"
2
+ # frozen_string_literal: true
3
+
4
+ require 'stable'
3
5
  Stable::CLI.start(ARGV)
data/lib/stable/cli.rb CHANGED
@@ -10,8 +10,6 @@ require_relative 'registry'
10
10
 
11
11
  module Stable
12
12
  class CLI < Thor
13
- HOSTS_FILE = '/etc/hosts'
14
-
15
13
  def initialize(*)
16
14
  super
17
15
  Stable::Bootstrap.run!
@@ -31,8 +29,10 @@ module Stable
31
29
  method_option :db, type: :string, desc: 'Database name to create and integrate'
32
30
  method_option :postgres, type: :boolean, default: false, desc: 'Use Postgres for the database'
33
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'
34
33
  def new(name, ruby: RUBY_VERSION, rails: nil, port: nil)
35
- Commands::New.new(name, options).call
34
+ safe_name = Validators::AppName.call!(name)
35
+ Commands::New.new(safe_name, options).call
36
36
  end
37
37
 
38
38
  desc 'list', 'List detected apps'
@@ -159,108 +159,8 @@ module Stable
159
159
  system("lsof -i tcp:#{port} > /dev/null 2>&1")
160
160
  end
161
161
 
162
- def generate_cert(domain)
163
- cert_path = File.join(Stable::Paths.certs_dir, "#{domain}.pem")
164
- key_path = File.join(Stable::Paths.certs_dir, "#{domain}-key.pem")
165
- FileUtils.mkdir_p(Stable::Paths.certs_dir)
166
-
167
- return if File.exist?(cert_path) && File.exist?(key_path)
168
-
169
- if system('which mkcert > /dev/null')
170
- system("mkcert -cert-file #{cert_path} -key-file #{key_path} #{domain}")
171
- else
172
- puts 'mkcert not found. Please install mkcert.'
173
- end
174
- end
175
-
176
- def update_caddyfile(domain, port)
177
- caddyfile = Stable::Paths.caddyfile
178
- FileUtils.touch(caddyfile) unless File.exist?(caddyfile)
179
- content = File.read(caddyfile)
180
-
181
- # remove existing block for domain
182
- regex = %r{
183
- https://#{Regexp.escape(domain)}\s*\{
184
- .*?
185
- \}
186
- }mx
187
-
188
- content = content.gsub(regex, '')
189
-
190
- # add new block
191
- cert_path = File.join(Stable::Paths.certs_dir, "#{domain}.pem")
192
- key_path = File.join(Stable::Paths.certs_dir, "#{domain}-key.pem")
193
- block = <<~CADDY
194
-
195
- https://#{domain} {
196
- reverse_proxy 127.0.0.1:#{port}
197
- tls #{cert_path} #{key_path}
198
- }
199
- CADDY
200
-
201
- File.write(caddyfile, content + block)
202
- system("caddy fmt --overwrite #{caddyfile}")
203
- end
204
-
205
- def ensure_certs_dir!
206
- certs_dir = Stable::Paths.certs_dir
207
- FileUtils.mkdir_p(certs_dir)
208
-
209
- begin
210
- FileUtils.chown_R(Etc.getlogin, nil, certs_dir)
211
- rescue StandardError => e
212
- puts "Could not change ownership: #{e.message}"
213
- end
214
-
215
- # Restrict permissions for security
216
- Dir.glob("#{certs_dir}/*.pem").each do |pem|
217
- FileUtils.chmod(0o600, pem)
218
- end
219
- end
220
-
221
- def wait_for_port(port, timeout: 20)
222
- require 'socket'
223
- start = Time.now
224
-
225
- loop do
226
- TCPSocket.new('127.0.0.1', port).close
227
- return
228
- rescue Errno::ECONNREFUSED
229
- raise "Rails never bound port #{port}. Check log/stable.log" if Time.now - start > timeout
230
-
231
- sleep 0.5
232
- end
233
- end
234
-
235
- # RVM/ruby helpers moved to Services::Ruby
236
-
237
- def app_running?(app)
238
- return false unless app && app[:port]
239
-
240
- system("lsof -i tcp:#{app[:port]} -sTCP:LISTEN > /dev/null 2>&1")
241
- end
242
-
243
- def boot_state(app)
244
- return 'stopped' unless app_running?(app)
245
-
246
- if app[:started_at]
247
- elapsed = Time.now.to_i - app[:started_at]
248
- return "booting (#{elapsed}s)" if elapsed < 10
249
- end
250
-
251
- 'running'
252
- end
253
-
254
162
  def dedupe_registry!
255
163
  Services::AppRegistry.dedupe
256
164
  end
257
-
258
- def gemset_for(app)
259
- Stable::Services::Ruby.gemset_for(app)
260
- end
261
-
262
- def rvm_exec(app, ruby)
263
- Stable::Services::Ruby.rvm_exec(app, ruby)
264
- end
265
165
  end
266
166
  end
@@ -21,14 +21,7 @@ module Stable
21
21
  private
22
22
 
23
23
  def print_header
24
- puts format(
25
- '%-18s %-26s %-8s %-10s %-10s',
26
- 'APP',
27
- 'DOMAIN',
28
- 'PORT',
29
- 'RUBY',
30
- 'STATUS'
31
- )
24
+ puts 'APP DOMAIN PORT RUBY STATUS '
32
25
  puts '-' * 78
33
26
  end
34
27
 
@@ -15,54 +15,54 @@ module Stable
15
15
  domain = "#{@name}.test"
16
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, pid: nil)
18
+ Services::AppRegistry.add(
19
+ name: @name, path: app_path, domain: domain, port: port,
20
+ ruby: ruby, started_at: nil, pid: nil
21
+ )
19
22
 
20
23
  abort "Folder already exists: #{app_path}" if File.exist?(app_path)
21
24
 
22
- # --- Ensure Ruby + gemset ---
25
+ # --- Ensure Ruby version & RVM ---
23
26
  Ruby.ensure_version(ruby)
24
27
  Ruby.ensure_rvm!
25
- System::Shell.run("bash -lc 'source #{Ruby.rvm_script} && rvm #{ruby}@#{@name} --create do true'")
28
+
29
+ # --- Create gemset ---
30
+ System::Shell.run("bash -lc 'source #{Ruby.rvm_script} && rvm #{ruby} do rvm gemset create #{@name} || true'")
26
31
 
27
32
  rvm_cmd = Ruby.rvm_prefix(ruby, @name)
28
33
 
29
- # --- Install Rails into gemset if missing ---
30
- rails_version = @options[:rails]
31
- rails_check_cmd = if rails_version
32
- "#{rvm_cmd} gem list -i rails -v #{rails_version}"
33
- else
34
- "#{rvm_cmd} gem list -i rails"
35
- end
34
+ # --- Install Bundler ---
35
+ System::Shell.run("bash -lc '#{rvm_cmd} gem install bundler --no-document'")
36
36
 
37
+ # --- Install Rails if missing ---
38
+ rails_version = @options[:rails]
39
+ rails_check_cmd = rails_version ? "#{rvm_cmd} gem list -i rails -v #{rails_version}" : "#{rvm_cmd} gem list -i rails"
37
40
  unless system("bash -lc '#{rails_check_cmd}'")
38
- puts "Installing Rails #{rails_version || 'latest'} in gemset..."
41
+ puts "Installing Rails #{rails_version || 'latest'}..."
39
42
  install_cmd = rails_version ? "#{rvm_cmd} gem install rails -v #{rails_version}" : "#{rvm_cmd} gem install rails"
40
- system("bash -lc '#{install_cmd}'") or abort('Failed to install Rails')
43
+ system("bash -lc '#{install_cmd}'") or abort("Failed to install Rails #{rails_version || ''}")
41
44
  end
42
45
 
43
46
  # --- Create Rails app ---
44
47
  puts "Creating Rails app #{@name} (Ruby #{ruby})..."
45
- System::Shell.run("bash -lc '#{rvm_cmd} rails new #{app_path}'")
48
+ System::Shell.run("bash -lc '#{rvm_cmd} rails new #{app_path} \
49
+ --skip-importmap \
50
+ --skip-hotwire \
51
+ --skip-javascript \
52
+ --skip-solid'")
46
53
 
47
- # --- Write ruby version/gemset and bundle install inside gemset ---
54
+ # --- Write ruby version/gemset ---
48
55
  Dir.chdir(app_path) do
49
56
  File.write('.ruby-version', "#{ruby}\n")
50
57
  File.write('.ruby-gemset', "#{@name}\n")
51
-
52
- puts 'Running bundle install...'
53
- System::Shell.run("bash -lc '#{rvm_cmd} bundle install --jobs=4 --retry=3'")
54
58
  end
55
59
 
56
60
  # --- Database integration ---
57
61
  if @options[:db]
58
- adapter = if @options[:mysql]
59
- :mysql
60
- else
61
- :postgresql
62
- end
63
-
62
+ adapter = @options[:mysql] ? :mysql : :postgresql
64
63
  gem_name = adapter == :postgresql ? 'pg' : 'mysql2'
65
64
  gemfile_path = File.join(app_path, 'Gemfile')
65
+
66
66
  unless File.read(gemfile_path).include?(gem_name)
67
67
  File.open(gemfile_path, 'a') do |f|
68
68
  f.puts "\n# Added by Stable CLI"
@@ -71,17 +71,85 @@ module Stable
71
71
  puts "✅ Added '#{gem_name}' gem to Gemfile"
72
72
  end
73
73
 
74
- # ensure gem is installed inside gemset
75
- System::Shell.run("bash -lc 'cd #{app_path} && #{rvm_cmd} bundle install --jobs=4 --retry=3'")
74
+ # Use correct Ruby/gemset for bundle install
75
+ System::Shell.run(rvm_run('bundle install --jobs=4 --retry=3', chdir: app_path))
76
76
 
77
- # run adapter setup which will write database.yml and prepare
78
77
  db_adapter = adapter == :mysql ? Database::MySQL : Database::Postgres
79
- db_adapter.new(app_name: @name, app_path: app_path).setup
78
+ db_adapter.new(app_name: @name, app_path: app_path, ruby: ruby).setup
79
+ end
80
+
81
+ # --- Run bundle & DB prepare ---
82
+ System::Shell.run(rvm_run('bundle install --jobs=4 --retry=3', chdir: app_path))
83
+ System::Shell.run(rvm_run('bundle exec rails db:prepare', chdir: app_path))
84
+
85
+ rails_version = `bash -lc 'cd #{app_path} && #{rvm_cmd} bundle exec rails runner "puts Rails.version"'`.strip.to_f
86
+
87
+ begin
88
+ rails_ver = Gem::Version.new(rails_version)
89
+ rescue ArgumentError
90
+ rails_ver = Gem::Version.new('0.0.0')
91
+ end
92
+
93
+ # only if Rails >= 7
94
+ if rails_ver >= Gem::Version.new('7.0.0')
95
+ # Gems to re-add
96
+ optional_gems = {
97
+ 'importmap-rails' => nil,
98
+ 'turbo-rails' => nil,
99
+ 'stimulus-rails' => nil,
100
+ 'solid_cache' => nil,
101
+ 'solid_queue' => nil,
102
+ 'solid_cable' => nil
103
+ }
104
+
105
+ gemfile = File.join(app_path, 'Gemfile')
106
+
107
+ # Add gems if not already present
108
+ optional_gems.each do |gem_name, version|
109
+ next if File.read(gemfile).match?(/^gem ['"]#{gem_name}['"]/)
110
+
111
+ File.open(gemfile, 'a') do |f|
112
+ f.puts version ? "gem '#{gem_name}', '#{version}'" : "gem '#{gem_name}'"
113
+ end
114
+ end
115
+
116
+ # Install all new gems
117
+ System::Shell.run(rvm_run('bundle install', chdir: app_path))
118
+
119
+ # Run generators for installed gems
120
+ generators = [
121
+ 'importmap:install',
122
+ 'turbo:install stimulus:install',
123
+ 'solid_cache:install solid_queue:install solid_cable:install'
124
+ ]
125
+
126
+ generators.each do |cmd|
127
+ System::Shell.run(rvm_run("bundle exec rails #{cmd}", chdir: app_path))
128
+ end
80
129
  end
81
130
 
82
- # --- Refresh bundle and prepare DB (idempotent) ---
83
- System::Shell.run("bash -lc 'cd #{app_path} && #{rvm_cmd} bundle check || #{rvm_cmd} bundle install'")
84
- System::Shell.run("bash -lc 'cd #{app_path} && #{rvm_cmd} bundle exec rails db:prepare'")
131
+ # --- Add allowed host for Rails < 7.2 ---
132
+ if @options[:rails] && @options[:rails].to_f < 7.2
133
+ env_file = File.join(app_path, 'config/environments/development.rb')
134
+
135
+ if File.exist?(env_file)
136
+ content = File.read(env_file)
137
+
138
+ # Append host config inside the Rails.application.configure block
139
+ updated_content = content.gsub(/Rails\.application\.configure do(.*?)end/mm) do |_match|
140
+ inner = Regexp.last_match(1)
141
+ # Prevent duplicate entries
142
+ unless inner.include?(domain)
143
+ inner += " # allow local .test host for this app\n config.hosts << '#{domain}'\n"
144
+ end
145
+ "Rails.application.configure do#{inner}end"
146
+ end
147
+
148
+ File.write(env_file, updated_content)
149
+ else
150
+ warn "Development environment file not found: #{env_file}"
151
+ end
152
+ end
85
153
 
86
154
  # --- Hosts, certs, caddy ---
87
155
  Services::HostsManager.add(domain)
@@ -89,50 +157,31 @@ module Stable
89
157
  Services::CaddyManager.ensure_running!
90
158
  Services::CaddyManager.reload
91
159
 
92
- # --- Start the app ---
160
+ # --- Start Rails server ---
93
161
  puts "Starting Rails server for #{@name} on port #{port}..."
94
162
  log_file = File.join(app_path, 'log', 'stable.log')
95
163
  FileUtils.mkdir_p(File.dirname(log_file))
96
164
 
97
165
  abort "Port #{port} is already in use. Choose another port." if port_in_use?(port)
98
166
 
99
- pid = spawn('bash', '-lc', "cd \"#{app_path}\" && #{rvm_cmd} bundle exec rails s -p #{port} -b 127.0.0.1",
100
- out: log_file, err: log_file)
167
+ pid = spawn(
168
+ 'bash', '-lc',
169
+ "cd \"#{app_path}\" && #{rvm_cmd} bundle exec rails s -p #{port} -b 127.0.0.1",
170
+ out: log_file, err: log_file
171
+ )
101
172
  Process.detach(pid)
102
-
103
173
  AppRegistry.update(@name, started_at: Time.now.to_i, pid: pid)
104
174
 
105
175
  sleep 1.5
106
176
  wait_for_port(port)
107
- prefix = @options[:skip_ssl] ? 'http' : 'https'
108
- display_domain = if @options[:skip_ssl]
109
- "#{domain}:#{port}"
110
- else
111
- domain
112
- end
113
177
 
178
+ prefix = @options[:skip_ssl] ? 'http' : 'https'
179
+ display_domain = @options[:skip_ssl] ? "#{domain}:#{port}" : domain
114
180
  puts "✔ #{@name} running at #{prefix}://#{display_domain}"
115
181
  end
116
182
 
117
183
  private
118
184
 
119
- def create_rails_app
120
- System::Shell.run("rails new #{@name}")
121
- end
122
-
123
- def setup_database
124
- return unless @options[:mysql] || @options[:postgres]
125
-
126
- adapter =
127
- if @options[:mysql]
128
- Database::MySQL
129
- else
130
- Database::Postgres
131
- end
132
-
133
- adapter.new(@name).setup
134
- end
135
-
136
185
  def next_free_port
137
186
  used_ports = Services::AppRegistry.all.map { |a| a[:port] }
138
187
  port = 3000
@@ -157,6 +206,13 @@ module Stable
157
206
  sleep 0.5
158
207
  end
159
208
  end
209
+
210
+ def rvm_run(cmd, chdir: nil, ruby: nil)
211
+ ruby ||= @options[:ruby] || RUBY_VERSION
212
+ gemset = @name
213
+ cd = chdir ? "cd #{chdir} && " : ''
214
+ "bash -lc '#{cd}source #{Dir.home}/.rvm/scripts/rvm && rvm #{ruby}@#{gemset} do #{cmd}'"
215
+ end
160
216
  end
161
217
  end
162
218
  end
@@ -131,6 +131,7 @@ module Stable
131
131
 
132
132
  until valid_pem?(path)
133
133
  raise "Invalid PEM file: #{path}" if Time.now - start > timeout
134
+
134
135
  sleep 0.1
135
136
  end
136
137
  end
@@ -142,11 +143,16 @@ module Stable
142
143
  begin
143
144
  FileUtils.chown_R(Etc.getlogin, nil, certs_dir)
144
145
  rescue StandardError
146
+ nil
145
147
  end
146
148
 
147
149
  Dir.glob("#{certs_dir}/*.pem").each do |pem|
148
150
  mode = pem.end_with?('-key.pem') ? 0o600 : 0o644
149
- FileUtils.chmod(mode, pem) rescue nil
151
+ begin
152
+ FileUtils.chmod(mode, pem)
153
+ rescue StandardError
154
+ nil
155
+ end
150
156
  end
151
157
  end
152
158
  end
@@ -4,14 +4,20 @@ module Stable
4
4
  module Services
5
5
  module Database
6
6
  class Base
7
- def initialize(app_name:, app_path:)
7
+ def initialize(app_name:, app_path:, ruby:)
8
8
  @app_name = app_name
9
9
  @app_path = app_path
10
+ @ruby = ruby
11
+ @database_name = @app_name
12
+ .downcase
13
+ .gsub(/[^a-z0-9_]/, '_')
14
+ .gsub(/_+/, '_')
15
+ .gsub(/^_+|_+$/, '')
10
16
  end
11
17
 
12
18
  def prepare
13
19
  System::Shell.run(
14
- "cd #{@app_path} && bundle exec rails db:prepare"
20
+ bash_rvm('bundle exec rails db:prepare')
15
21
  )
16
22
  end
17
23
 
@@ -22,10 +28,10 @@ module Stable
22
28
  'default' => base_config(creds),
23
29
  'development' => base_config(creds),
24
30
  'test' => base_config(creds).merge(
25
- 'database' => "#{@app_name}_test"
31
+ 'database' => "#{@database_name}_test"
26
32
  ),
27
33
  'production' => base_config(creds).merge(
28
- 'database' => "#{@app_name}_production"
34
+ 'database' => "#{@database_name}_production"
29
35
  )
30
36
  }
31
37
 
@@ -36,6 +42,14 @@ module Stable
36
42
  def base_config(_creds)
37
43
  raise NotImplementedError
38
44
  end
45
+
46
+ def bash_rvm(cmd)
47
+ raise 'ruby not set' unless @ruby
48
+ raise 'app_name not set' unless @app_name
49
+ raise 'app_path not set' unless @app_path
50
+
51
+ "bash -lc 'cd #{@app_path} && source #{Ruby.rvm_script} && rvm #{@ruby}@#{@app_name} do #{cmd}'"
52
+ end
39
53
  end
40
54
  end
41
55
  end
@@ -13,7 +13,7 @@ module Stable
13
13
 
14
14
  def create_database(creds)
15
15
  System::Shell.run(
16
- "mysql -u #{creds[:user]} -p#{creds[:password]} -e 'CREATE DATABASE IF NOT EXISTS #{@app_name};'"
16
+ "mysql -u #{creds[:user]} -p#{creds[:password]} -e 'CREATE DATABASE IF NOT EXISTS #{@database_name};'"
17
17
  )
18
18
  end
19
19
 
@@ -24,7 +24,7 @@ module Stable
24
24
  'adapter' => 'mysql2',
25
25
  'encoding' => 'utf8mb4',
26
26
  'pool' => 5,
27
- 'database' => @app_name,
27
+ 'database' => @database_name,
28
28
  'username' => creds[:user],
29
29
  'password' => creds[:password],
30
30
  'host' => 'localhost'
@@ -5,7 +5,7 @@ module Stable
5
5
  module Database
6
6
  class Postgres < Base
7
7
  def setup
8
- System::Shell.run("createdb #{@app_name}")
8
+ System::Shell.run("createdb #{@database_name}")
9
9
  prepare
10
10
  end
11
11
  end
@@ -37,7 +37,7 @@ module Stable
37
37
  rvm_script = File.expand_path('~/.rvm/scripts/rvm')
38
38
  abort 'RVM installed but could not be loaded' unless File.exist?(rvm_script)
39
39
 
40
- ENV['PATH'] = "#{File.expand_path('~/.rvm/bin')}:#{ENV['PATH']}"
40
+ ENV['PATH'] = "#{File.expand_path('~/.rvm/bin')}:#{ENV.fetch('PATH', nil)}"
41
41
 
42
42
  system(%(bash -lc "source #{rvm_script} && rvm --version")) || abort('RVM installed but not functional')
43
43
  end
@@ -97,6 +97,15 @@ module Stable
97
97
  "rvm #{ruby} do"
98
98
  end
99
99
  end
100
+
101
+ def self.clean_rvm_exec(ruby, gemset, cmd)
102
+ <<~CMD
103
+ bash -lc '
104
+ source #{rvm_script};
105
+ rvm #{ruby}@#{gemset} --create do #{cmd}
106
+ '
107
+ CMD
108
+ end
100
109
  end
101
110
  end
102
111
  end
@@ -21,7 +21,7 @@ module Stable
21
21
 
22
22
  def ensure_directories
23
23
  path = Stable::Paths.certs_dir
24
- FileUtils.mkdir_p(path) unless Dir.exist?(path)
24
+ FileUtils.mkdir_p(path)
25
25
  end
26
26
 
27
27
  def ensure_apps_registry
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stable
4
+ module Validators
5
+ class AppName
6
+ VALID_PATTERN = /\A[a-z0-9]+(-[a-z0-9]+)*\z/
7
+ MAX_LENGTH = 63
8
+
9
+ def self.call!(name)
10
+ normalized = normalize(name)
11
+
12
+ unless valid?(normalized)
13
+ raise Thor::Error, <<~MSG
14
+ Invalid app name: "#{name}"
15
+
16
+ Use only:
17
+ - lowercase letters (a-z)
18
+ - numbers (0-9)
19
+ - hyphens (-)
20
+
21
+ Rules:
22
+ - no spaces or underscores
23
+ - cannot start or end with a hyphen
24
+ - max #{MAX_LENGTH} characters
25
+
26
+ Example:
27
+ stable new my-app
28
+ MSG
29
+ end
30
+
31
+ normalized
32
+ end
33
+
34
+ def self.normalize(name)
35
+ name
36
+ .downcase
37
+ .strip
38
+ .gsub(/\s+/, '-') # spaces → hyphens
39
+ .gsub(/[^a-z0-9-]/, '') # drop invalid chars
40
+ .gsub(/-+/, '-') # collapse hyphens
41
+ .gsub(/\A-|-+\z/, '') # trim hyphens
42
+ end
43
+
44
+ def self.valid?(name)
45
+ name.length <= MAX_LENGTH && VALID_PATTERN.match?(name)
46
+ end
47
+ end
48
+ end
49
+ end
data/lib/stable.rb CHANGED
@@ -14,20 +14,22 @@ require_relative 'stable/scanner'
14
14
  require_relative 'stable/bootstrap'
15
15
  require_relative 'stable/db_manager'
16
16
 
17
- Dir[File.join(__dir__, 'stable', 'services', '**', '*.rb')].sort.each { |f| require f }
18
- Dir[File.join(__dir__, 'stable', 'commands', '**', '*.rb')].sort.each { |f| require f }
19
- Dir[File.join(__dir__, 'stable', 'system', '**', '*.rb')].sort.each { |f| require f }
20
- Dir[File.join(__dir__, 'stable', 'utils', '**', '*.rb')].sort.each { |f| require f }
21
- Dir[File.join(__dir__, 'stable', 'config', '**', '*.rb')].sort.each { |f| require f }
17
+ Dir[File.join(__dir__, 'stable', 'services', '**', '*.rb')].each { |f| require f }
18
+ Dir[File.join(__dir__, 'stable', 'commands', '**', '*.rb')].each { |f| require f }
19
+ Dir[File.join(__dir__, 'stable', 'system', '**', '*.rb')].each { |f| require f }
20
+ Dir[File.join(__dir__, 'stable', 'utils', '**', '*.rb')].each { |f| require f }
21
+ Dir[File.join(__dir__, 'stable', 'config', '**', '*.rb')].each { |f| require f }
22
+ Dir[File.join(__dir__, 'stable', 'validators', '**', '*.rb')].each { |f| require f }
22
23
 
23
- AppRegistry = Stable::Services::AppRegistry unless defined?(::AppRegistry)
24
- HostsManager = Stable::Services::HostsManager unless defined?(::HostsManager)
25
- CaddyManager = Stable::Services::CaddyManager unless defined?(::CaddyManager)
26
- ProcessManager = Stable::Services::ProcessManager unless defined?(::ProcessManager)
27
- AppCreator = Stable::Services::AppCreator unless defined?(::AppCreator)
28
- AppStarter = Stable::Services::AppStarter unless defined?(::AppStarter)
29
- AppStopper = Stable::Services::AppStopper unless defined?(::AppStopper)
30
- AppRemover = Stable::Services::AppRemover unless defined?(::AppRemover)
31
- Database = Stable::Services::Database unless defined?(::Database)
32
- Ruby = Stable::Services::Ruby unless defined?(::Ruby)
33
- Commands = Stable::Commands unless defined?(::Commands)
24
+ AppRegistry = Stable::Services::AppRegistry unless defined?(AppRegistry)
25
+ HostsManager = Stable::Services::HostsManager unless defined?(HostsManager)
26
+ CaddyManager = Stable::Services::CaddyManager unless defined?(CaddyManager)
27
+ ProcessManager = Stable::Services::ProcessManager unless defined?(ProcessManager)
28
+ AppCreator = Stable::Services::AppCreator unless defined?(AppCreator)
29
+ AppStarter = Stable::Services::AppStarter unless defined?(AppStarter)
30
+ AppStopper = Stable::Services::AppStopper unless defined?(AppStopper)
31
+ AppRemover = Stable::Services::AppRemover unless defined?(AppRemover)
32
+ Database = Stable::Services::Database unless defined?(Database)
33
+ Ruby = Stable::Services::Ruby unless defined?(Ruby)
34
+ Commands = Stable::Commands unless defined?(Commands)
35
+ Validators = Stable::Validators unless defined?(Validators)
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.7.10
4
+ version: 0.7.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danny Simfukwe
@@ -94,10 +94,12 @@ files:
94
94
  - lib/stable/services/setup_runner.rb
95
95
  - lib/stable/system/shell.rb
96
96
  - lib/stable/utils/prompts.rb
97
+ - lib/stable/validators/app_name.rb
97
98
  homepage: https://github.com/dannysimfukwe/stable-rails
98
99
  licenses:
99
100
  - MIT
100
- metadata: {}
101
+ metadata:
102
+ rubygems_mfa_required: 'true'
101
103
  rdoc_options: []
102
104
  require_paths:
103
105
  - lib