stable-cli-rails 0.7.10 → 0.7.11

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: fe9114ae5eae07811ec911b967cef33620ebf62c7df4f30c202f3743f7d6bea5
4
+ data.tar.gz: 91920b483e2c26e58d1ef7ca7c786ea5cf91a4849ac16ec276fee4cdbf72d878
5
5
  SHA512:
6
- metadata.gz: 41399496362c590019b87351d9e69364b41e6a3ecf0f10a7906a1c3edbaa508557104bd33cfb855623ee0b1e75e690b4d7834a42bd90025bfc5f23bc28dd2bf3
7
- data.tar.gz: e4779489d26bd0b472a3a6f545039296bb7481aa87df1dbfa0748acd440f3e78b79f091b318956d6bc92aae8e7179f1f66393f9d800fff5b015a47bb3c2edc45
6
+ metadata.gz: dbdc4b736300bd5a395f67b9138e3aa7410a0184d4f5ea2be6f5e3445934fcf57588b526e5b957ee0c551247ab3013febd29eaffd96cc7bde43f982df91e3b53
7
+ data.tar.gz: 19e8dd338cd34dd6621159785171c970468d43b0f423048c4c3fc10bd4ab4851cedd59091a03e1748acf990fa1853a7b1da33bbadba552bae186a71a69799f3e
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!
@@ -32,7 +30,8 @@ module Stable
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'
34
32
  def new(name, ruby: RUBY_VERSION, rails: nil, port: nil)
35
- Commands::New.new(name, options).call
33
+ safe_name = Validators::AppName.call!(name)
34
+ Commands::New.new(safe_name, options).call
36
35
  end
37
36
 
38
37
  desc 'list', 'List detected apps'
@@ -159,108 +158,8 @@ module Stable
159
158
  system("lsof -i tcp:#{port} > /dev/null 2>&1")
160
159
  end
161
160
 
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
161
  def dedupe_registry!
255
162
  Services::AppRegistry.dedupe
256
163
  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
164
  end
266
165
  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,7 +15,8 @@ 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(name: @name, path: app_path, domain: domain, port: port, ruby: ruby, started_at: nil,
19
+ pid: nil)
19
20
 
20
21
  abort "Folder already exists: #{app_path}" if File.exist?(app_path)
21
22
 
@@ -106,10 +107,10 @@ module Stable
106
107
  wait_for_port(port)
107
108
  prefix = @options[:skip_ssl] ? 'http' : 'https'
108
109
  display_domain = if @options[:skip_ssl]
109
- "#{domain}:#{port}"
110
- else
111
- domain
112
- end
110
+ "#{domain}:#{port}"
111
+ else
112
+ domain
113
+ end
113
114
 
114
115
  puts "✔ #{@name} running at #{prefix}://#{display_domain}"
115
116
  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
@@ -7,6 +7,11 @@ module Stable
7
7
  def initialize(app_name:, app_path:)
8
8
  @app_name = app_name
9
9
  @app_path = app_path
10
+ @database_name = @app_name
11
+ .downcase
12
+ .gsub(/[^a-z0-9_]/, '_')
13
+ .gsub(/_+/, '_')
14
+ .gsub(/^_+|_+$/, '')
10
15
  end
11
16
 
12
17
  def prepare
@@ -22,10 +27,10 @@ module Stable
22
27
  'default' => base_config(creds),
23
28
  'development' => base_config(creds),
24
29
  'test' => base_config(creds).merge(
25
- 'database' => "#{@app_name}_test"
30
+ 'database' => "#{@database_name}_test"
26
31
  ),
27
32
  'production' => base_config(creds).merge(
28
- 'database' => "#{@app_name}_production"
33
+ 'database' => "#{@database_name}_production"
29
34
  )
30
35
  }
31
36
 
@@ -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
@@ -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.11
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