stable-cli-rails 0.7.12 → 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.
- checksums.yaml +4 -4
- data/lib/stable/bootstrap.rb +1 -0
- data/lib/stable/cli.rb +1 -5
- data/lib/stable/commands/doctor.rb +1 -0
- data/lib/stable/commands/list.rb +7 -6
- data/lib/stable/commands/new.rb +1 -0
- data/lib/stable/commands/remove.rb +1 -0
- data/lib/stable/commands/restart.rb +1 -0
- data/lib/stable/commands/setup.rb +1 -0
- data/lib/stable/commands/start.rb +1 -0
- data/lib/stable/commands/stop.rb +1 -0
- data/lib/stable/db_manager.rb +22 -7
- data/lib/stable/paths.rb +10 -1
- data/lib/stable/registry.rb +57 -7
- data/lib/stable/scanner.rb +1 -0
- data/lib/stable/services/app_creator.rb +11 -9
- data/lib/stable/services/app_registry.rb +7 -19
- data/lib/stable/services/app_remover.rb +1 -0
- data/lib/stable/services/app_restarter.rb +1 -0
- data/lib/stable/services/app_starter.rb +1 -0
- data/lib/stable/services/app_stopper.rb +1 -0
- data/lib/stable/services/caddy_manager.rb +1 -0
- data/lib/stable/services/database/base.rb +1 -0
- data/lib/stable/services/database/mysql.rb +1 -0
- data/lib/stable/services/database/postgres.rb +1 -0
- data/lib/stable/services/dependency_checker.rb +58 -9
- data/lib/stable/services/hosts_manager.rb +18 -7
- data/lib/stable/services/process_manager.rb +1 -0
- data/lib/stable/services/ruby.rb +1 -0
- data/lib/stable/services/setup_runner.rb +110 -16
- data/lib/stable/system/shell.rb +1 -0
- data/lib/stable/utils/package_manager.rb +68 -0
- data/lib/stable/utils/platform.rb +76 -0
- data/lib/stable/utils/prompts.rb +1 -0
- data/lib/stable/validators/app_name.rb +1 -0
- data/lib/stable.rb +1 -0
- metadata +6 -31
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab8c647151029030baf0e96737f45f20cc6b067b952b032b98eac46e5e14e158
|
|
4
|
+
data.tar.gz: 5db558cabc869e1f6d6684d26b1d7b08178679194d09ebdbbd42aa67d828aee7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fe68968ac73be6725262a224ceda502eaee576c2aa3a8822a2742577e9f5d6fc05f1c3092c6f7f1c0f011218aaa851949863e3473e8ea95de9e6fd91b94f31dc
|
|
7
|
+
data.tar.gz: e333ac1da62ceb50a7a77ec83d1c145900f90b8888249c4307102bc1dfb0e3661e0bf97c1efe5a33be27b1aa409e639278ca91643989c3c2c0954d763c665533
|
data/lib/stable/bootstrap.rb
CHANGED
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?
|
|
@@ -158,9 +158,5 @@ module Stable
|
|
|
158
158
|
def port_in_use?(port)
|
|
159
159
|
system("lsof -i tcp:#{port} > /dev/null 2>&1")
|
|
160
160
|
end
|
|
161
|
-
|
|
162
|
-
def dedupe_registry!
|
|
163
|
-
Services::AppRegistry.dedupe
|
|
164
|
-
end
|
|
165
161
|
end
|
|
166
162
|
end
|
data/lib/stable/commands/list.rb
CHANGED
|
@@ -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
|
-
'
|
|
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
|
data/lib/stable/commands/new.rb
CHANGED
data/lib/stable/commands/stop.rb
CHANGED
data/lib/stable/db_manager.rb
CHANGED
|
@@ -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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
data/lib/stable/registry.rb
CHANGED
|
@@ -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.
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
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
|
data/lib/stable/scanner.rb
CHANGED
|
@@ -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,17 +12,19 @@ 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.
|
|
15
|
+
app_path = File.join(Stable::Paths.projects_dir, @name)
|
|
15
16
|
domain = "#{@name}.test"
|
|
16
17
|
|
|
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)
|
|
21
|
+
|
|
17
22
|
# --- Register app in registry ---
|
|
18
23
|
Services::AppRegistry.add(
|
|
19
24
|
name: @name, path: app_path, domain: domain, port: port,
|
|
20
25
|
ruby: ruby, started_at: nil, pid: nil
|
|
21
26
|
)
|
|
22
27
|
|
|
23
|
-
abort "Folder already exists: #{app_path}" if File.exist?(app_path)
|
|
24
|
-
|
|
25
28
|
# --- Ensure Ruby version & RVM ---
|
|
26
29
|
Ruby.ensure_version(ruby)
|
|
27
30
|
Ruby.ensure_rvm!
|
|
@@ -45,11 +48,9 @@ module Stable
|
|
|
45
48
|
|
|
46
49
|
# --- Create Rails app ---
|
|
47
50
|
puts "Creating Rails app #{@name} (Ruby #{ruby})..."
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
--skip-javascript \
|
|
52
|
-
--skip-solid'")
|
|
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}'")
|
|
53
54
|
|
|
54
55
|
# --- Write ruby version/gemset ---
|
|
55
56
|
Dir.chdir(app_path) do
|
|
@@ -82,7 +83,8 @@ module Stable
|
|
|
82
83
|
System::Shell.run(rvm_run('bundle install --jobs=4 --retry=3', chdir: app_path))
|
|
83
84
|
System::Shell.run(rvm_run('bundle exec rails db:prepare', chdir: app_path))
|
|
84
85
|
|
|
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
|
|
86
88
|
|
|
87
89
|
begin
|
|
88
90
|
rails_ver = Gem::Version.new(rails_version)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
61
|
-
Stable::Registry.save(apps)
|
|
50
|
+
Stable::Registry.remove_app_config(name)
|
|
62
51
|
end
|
|
63
52
|
|
|
64
53
|
def update(name, attrs)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return unless idx
|
|
54
|
+
app = find(name)
|
|
55
|
+
return unless app
|
|
68
56
|
|
|
69
|
-
|
|
70
|
-
Stable::Registry.
|
|
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)
|
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../utils/package_manager'
|
|
4
|
+
|
|
3
5
|
module Stable
|
|
4
6
|
module Services
|
|
7
|
+
# Service for checking system dependencies and health
|
|
5
8
|
class DependencyChecker
|
|
6
9
|
def run
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
platform = Stable::Utils::Platform.current
|
|
11
|
+
package_manager_name = Stable::Utils::PackageManager.name
|
|
12
|
+
|
|
13
|
+
checks = [
|
|
14
|
+
check(package_manager_name, package_manager_command(platform)),
|
|
9
15
|
check('Caddy', 'caddy'),
|
|
10
16
|
check('mkcert', 'mkcert'),
|
|
11
|
-
|
|
17
|
+
check_ruby_manager,
|
|
12
18
|
check_caddy_running,
|
|
13
19
|
check_certs_dir,
|
|
14
20
|
check_apps_registry
|
|
15
21
|
]
|
|
22
|
+
|
|
23
|
+
checks.compact
|
|
16
24
|
end
|
|
17
25
|
|
|
18
26
|
private
|
|
19
27
|
|
|
20
28
|
def check(name, command)
|
|
29
|
+
return nil if command.nil? # Skip checks that don't apply to this platform
|
|
30
|
+
|
|
21
31
|
ok = system("which #{command} > /dev/null 2>&1")
|
|
22
32
|
{
|
|
23
33
|
name: name,
|
|
@@ -26,8 +36,47 @@ module Stable
|
|
|
26
36
|
}
|
|
27
37
|
end
|
|
28
38
|
|
|
39
|
+
def package_manager_command(platform)
|
|
40
|
+
case platform
|
|
41
|
+
when :macos
|
|
42
|
+
'brew'
|
|
43
|
+
when :linux
|
|
44
|
+
case Stable::Utils::Platform.package_manager
|
|
45
|
+
when :apt
|
|
46
|
+
'apt'
|
|
47
|
+
when :yum
|
|
48
|
+
'yum'
|
|
49
|
+
when :pacman
|
|
50
|
+
'pacman'
|
|
51
|
+
end
|
|
52
|
+
when :windows
|
|
53
|
+
nil # No package manager check for Windows
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def check_ruby_manager
|
|
58
|
+
managers = [%w[rvm RVM], %w[rbenv rbenv], %w[chruby chruby]]
|
|
59
|
+
|
|
60
|
+
manager = managers.find do |cmd, _name|
|
|
61
|
+
system("which #{cmd} > /dev/null 2>&1")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if manager
|
|
65
|
+
{ name: manager[1], ok: true, message: nil }
|
|
66
|
+
else
|
|
67
|
+
{ name: 'Ruby version manager', ok: false, message: 'No Ruby version manager found (RVM, rbenv, or chruby)' }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
29
71
|
def check_caddy_running
|
|
30
|
-
|
|
72
|
+
platform = Stable::Utils::Platform.current
|
|
73
|
+
cmd = if platform == :windows
|
|
74
|
+
'tasklist /FI "IMAGENAME eq caddy.exe" 2>NUL | find /I "caddy.exe" >NUL'
|
|
75
|
+
else
|
|
76
|
+
'pgrep caddy > /dev/null 2>&1'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
ok = system(cmd)
|
|
31
80
|
{
|
|
32
81
|
name: 'Caddy running',
|
|
33
82
|
ok: ok,
|
|
@@ -36,7 +85,7 @@ module Stable
|
|
|
36
85
|
end
|
|
37
86
|
|
|
38
87
|
def check_certs_dir
|
|
39
|
-
path =
|
|
88
|
+
path = Stable::Paths.certs_dir
|
|
40
89
|
ok = Dir.exist?(path)
|
|
41
90
|
{
|
|
42
91
|
name: 'Certificates directory',
|
|
@@ -46,12 +95,12 @@ module Stable
|
|
|
46
95
|
end
|
|
47
96
|
|
|
48
97
|
def check_apps_registry
|
|
49
|
-
path =
|
|
50
|
-
ok =
|
|
98
|
+
path = Stable::Paths.projects_dir
|
|
99
|
+
ok = Dir.exist?(path)
|
|
51
100
|
{
|
|
52
|
-
name: '
|
|
101
|
+
name: 'Projects directory',
|
|
53
102
|
ok: ok,
|
|
54
|
-
message: ok ? nil : 'Missing
|
|
103
|
+
message: ok ? nil : 'Missing projects directory. Run `stable setup`'
|
|
55
104
|
}
|
|
56
105
|
end
|
|
57
106
|
end
|
|
@@ -1,29 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../utils/platform'
|
|
4
|
+
|
|
3
5
|
module Stable
|
|
4
6
|
module Services
|
|
7
|
+
# Service for managing hosts file entries
|
|
5
8
|
class HostsManager
|
|
6
|
-
|
|
9
|
+
def self.hosts_file
|
|
10
|
+
Stable::Utils::Platform.hosts_file
|
|
11
|
+
end
|
|
7
12
|
|
|
8
13
|
def self.remove(domain)
|
|
9
|
-
|
|
14
|
+
hosts_file = hosts_file()
|
|
15
|
+
lines = File.read(hosts_file).lines
|
|
10
16
|
filtered = lines.reject { |l| l.include?(domain) }
|
|
11
17
|
|
|
12
18
|
return if lines == filtered
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
if Process.uid.zero?
|
|
21
|
+
File.write(hosts_file, filtered.join)
|
|
22
|
+
else
|
|
23
|
+
File.write('/tmp/hosts', filtered.join)
|
|
24
|
+
system("sudo mv /tmp/hosts #{hosts_file}")
|
|
25
|
+
end
|
|
16
26
|
end
|
|
17
27
|
|
|
18
28
|
def self.add(domain)
|
|
29
|
+
hosts_file = hosts_file()
|
|
19
30
|
entry = "127.0.0.1\t#{domain}\n"
|
|
20
|
-
hosts = File.read(
|
|
31
|
+
hosts = File.read(hosts_file)
|
|
21
32
|
return if hosts.include?(domain)
|
|
22
33
|
|
|
23
34
|
if Process.uid.zero?
|
|
24
|
-
File.open(
|
|
35
|
+
File.open(hosts_file, 'a') { |f| f.puts entry }
|
|
25
36
|
else
|
|
26
|
-
system(%(echo "#{entry}" | sudo tee -a #{
|
|
37
|
+
system(%(echo "#{entry}" | sudo tee -a #{hosts_file} > /dev/null))
|
|
27
38
|
end
|
|
28
39
|
end
|
|
29
40
|
end
|
data/lib/stable/services/ruby.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../utils/package_manager'
|
|
4
|
+
|
|
3
5
|
module Stable
|
|
4
6
|
module Services
|
|
7
|
+
# Service for setting up the Stable environment and dependencies
|
|
5
8
|
class SetupRunner
|
|
6
9
|
def call
|
|
7
10
|
ensure_directories
|
|
@@ -25,8 +28,8 @@ module Stable
|
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
def ensure_apps_registry
|
|
28
|
-
path = Stable::Paths.
|
|
29
|
-
|
|
31
|
+
path = Stable::Paths.projects_dir
|
|
32
|
+
FileUtils.mkdir_p(path)
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
def ensure_caddyfile
|
|
@@ -41,42 +44,133 @@ module Stable
|
|
|
41
44
|
end
|
|
42
45
|
|
|
43
46
|
def ensure_dependencies!
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
platform = Stable::Utils::Platform.current
|
|
48
|
+
|
|
49
|
+
unless Stable::Utils::PackageManager.available?
|
|
50
|
+
puts "#{Stable::Utils::PackageManager.name} package manager is required."
|
|
51
|
+
show_platform_installation_instructions(platform)
|
|
46
52
|
exit 1
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
# --- Install Caddy ---
|
|
50
|
-
unless system('which caddy > /dev/null')
|
|
56
|
+
unless system('which caddy > /dev/null 2>&1')
|
|
51
57
|
puts 'Installing Caddy...'
|
|
52
|
-
|
|
58
|
+
install_package('caddy')
|
|
53
59
|
end
|
|
54
60
|
|
|
55
61
|
# --- Install mkcert ---
|
|
56
|
-
unless system('which mkcert > /dev/null')
|
|
62
|
+
unless system('which mkcert > /dev/null 2>&1')
|
|
57
63
|
puts 'Installing mkcert...'
|
|
58
|
-
|
|
64
|
+
install_mkcert(platform)
|
|
59
65
|
end
|
|
60
66
|
|
|
61
|
-
# Always ensure mkcert CA is installed
|
|
62
|
-
|
|
67
|
+
# Always ensure mkcert CA is installed (skip on Windows for now)
|
|
68
|
+
unless platform == :windows
|
|
69
|
+
begin
|
|
70
|
+
system('mkcert -install')
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
63
75
|
|
|
64
76
|
# --- Install PostgreSQL ---
|
|
65
|
-
unless system('which psql > /dev/null')
|
|
77
|
+
unless system('which psql > /dev/null 2>&1')
|
|
66
78
|
puts 'Installing PostgreSQL...'
|
|
67
|
-
|
|
68
|
-
system('brew services start postgresql')
|
|
79
|
+
install_postgres(platform)
|
|
69
80
|
end
|
|
70
81
|
|
|
71
82
|
# --- Install MySQL ---
|
|
72
|
-
unless system('which mysql > /dev/null')
|
|
83
|
+
unless system('which mysql > /dev/null 2>&1')
|
|
73
84
|
puts 'Installing MySQL...'
|
|
74
|
-
|
|
75
|
-
system('brew services start mysql')
|
|
85
|
+
install_mysql(platform)
|
|
76
86
|
end
|
|
77
87
|
|
|
78
88
|
puts '✅ All dependencies are installed and running.'
|
|
79
89
|
end
|
|
90
|
+
|
|
91
|
+
def install_package(package)
|
|
92
|
+
cmd = Stable::Utils::PackageManager.install_command(package)
|
|
93
|
+
system(cmd) or abort("Failed to install #{package}")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def install_mkcert(platform)
|
|
97
|
+
case platform
|
|
98
|
+
when :macos
|
|
99
|
+
install_package('mkcert nss')
|
|
100
|
+
when :linux
|
|
101
|
+
case Stable::Utils::Platform.package_manager
|
|
102
|
+
when :apt
|
|
103
|
+
system('sudo apt update && sudo apt install -y wget')
|
|
104
|
+
system('wget -O mkcert https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64')
|
|
105
|
+
system('chmod +x mkcert && sudo mv mkcert /usr/local/bin/')
|
|
106
|
+
when :yum
|
|
107
|
+
system('wget -O mkcert https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64')
|
|
108
|
+
system('chmod +x mkcert && sudo mv mkcert /usr/local/bin/')
|
|
109
|
+
end
|
|
110
|
+
when :windows
|
|
111
|
+
puts 'Please install mkcert manually on Windows: https://github.com/FiloSottile/mkcert'
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def install_postgres(platform)
|
|
116
|
+
case platform
|
|
117
|
+
when :macos
|
|
118
|
+
install_package('postgresql')
|
|
119
|
+
begin
|
|
120
|
+
system('brew services start postgresql')
|
|
121
|
+
rescue StandardError
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
when :linux
|
|
125
|
+
install_package('postgresql postgresql-contrib')
|
|
126
|
+
start_service('postgresql')
|
|
127
|
+
when :windows
|
|
128
|
+
puts 'Please install PostgreSQL manually on Windows from: https://www.postgresql.org/download/windows/'
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def install_mysql(platform)
|
|
133
|
+
case platform
|
|
134
|
+
when :macos
|
|
135
|
+
install_package('mysql')
|
|
136
|
+
begin
|
|
137
|
+
system('brew services start mysql')
|
|
138
|
+
rescue StandardError
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
when :linux
|
|
142
|
+
install_package('mysql-server')
|
|
143
|
+
start_service('mysql')
|
|
144
|
+
when :windows
|
|
145
|
+
puts 'Please install MySQL manually on Windows from: https://dev.mysql.com/downloads/mysql/'
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def start_service(service)
|
|
150
|
+
cmd = Stable::Utils::PackageManager.service_start_command(service)
|
|
151
|
+
begin
|
|
152
|
+
system(cmd)
|
|
153
|
+
rescue StandardError
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def show_platform_installation_instructions(platform)
|
|
159
|
+
case platform
|
|
160
|
+
when :macos
|
|
161
|
+
puts 'Homebrew is required. Install it first: https://brew.sh'
|
|
162
|
+
when :linux
|
|
163
|
+
puts 'Please install a package manager:'
|
|
164
|
+
puts ' Ubuntu/Debian: sudo apt update && sudo apt install -y build-essential'
|
|
165
|
+
puts ' CentOS/RHEL: sudo yum install -y gcc gcc-c++ make'
|
|
166
|
+
puts ' Arch: sudo pacman -S base-devel'
|
|
167
|
+
when :windows
|
|
168
|
+
puts 'Please install dependencies manually on Windows.'
|
|
169
|
+
puts 'Required: Caddy, mkcert, PostgreSQL, MySQL'
|
|
170
|
+
else
|
|
171
|
+
puts 'Unsupported platform detected.'
|
|
172
|
+
end
|
|
173
|
+
end
|
|
80
174
|
end
|
|
81
175
|
end
|
|
82
176
|
end
|
data/lib/stable/system/shell.rb
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'platform'
|
|
4
|
+
|
|
5
|
+
module Stable
|
|
6
|
+
module Utils
|
|
7
|
+
# Cross-platform package manager utilities
|
|
8
|
+
module PackageManager
|
|
9
|
+
class << self
|
|
10
|
+
def install_command(package)
|
|
11
|
+
case Platform.package_manager
|
|
12
|
+
when :brew
|
|
13
|
+
"brew install #{package}"
|
|
14
|
+
when :apt
|
|
15
|
+
"sudo apt update && sudo apt install -y #{package}"
|
|
16
|
+
when :yum
|
|
17
|
+
"sudo yum install -y #{package}"
|
|
18
|
+
when :pacman
|
|
19
|
+
"sudo pacman -S --noconfirm #{package}"
|
|
20
|
+
else
|
|
21
|
+
raise "Unsupported package manager on #{Platform.current} platform"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def service_start_command(service)
|
|
26
|
+
case Platform.current
|
|
27
|
+
when :macos
|
|
28
|
+
"brew services start #{service}"
|
|
29
|
+
when :linux
|
|
30
|
+
"sudo systemctl start #{service}" if systemctl_available?
|
|
31
|
+
else
|
|
32
|
+
raise "Service management not supported on #{Platform.current} platform"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def available?
|
|
37
|
+
pm = Platform.package_manager
|
|
38
|
+
if pm == :yum
|
|
39
|
+
system('which yum > /dev/null 2>&1') || system('which dnf > /dev/null 2>&1')
|
|
40
|
+
else
|
|
41
|
+
cmd = package_manager_commands.dig(pm, 0)
|
|
42
|
+
cmd ? system("#{cmd} > /dev/null 2>&1") : false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def name
|
|
47
|
+
pm = Platform.package_manager
|
|
48
|
+
package_manager_commands[pm]&.last || 'Unknown'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def package_manager_commands
|
|
54
|
+
{
|
|
55
|
+
brew: ['which brew', 'Homebrew'],
|
|
56
|
+
apt: ['which apt', 'APT'],
|
|
57
|
+
yum: ['which yum', 'YUM/DNF'],
|
|
58
|
+
pacman: ['which pacman', 'Pacman']
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def systemctl_available?
|
|
63
|
+
system('which systemctl > /dev/null 2>&1')
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stable
|
|
4
|
+
module Utils
|
|
5
|
+
# Platform detection utilities for cross-platform compatibility
|
|
6
|
+
module Platform
|
|
7
|
+
class << self
|
|
8
|
+
def macos?
|
|
9
|
+
!!(RUBY_PLATFORM =~ /darwin/)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def linux?
|
|
13
|
+
!!(RUBY_PLATFORM =~ /linux/)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def windows?
|
|
17
|
+
!!(RUBY_PLATFORM =~ /mingw|mswin|win32/)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def unix?
|
|
21
|
+
!windows?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def current
|
|
25
|
+
return :macos if macos?
|
|
26
|
+
return :linux if linux?
|
|
27
|
+
return :windows if windows?
|
|
28
|
+
|
|
29
|
+
:unknown
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def package_manager
|
|
33
|
+
return :brew if macos?
|
|
34
|
+
return detect_linux_package_manager if linux?
|
|
35
|
+
|
|
36
|
+
:unknown
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def hosts_file
|
|
40
|
+
return '/etc/hosts' if unix?
|
|
41
|
+
return 'C:\Windows\System32\drivers\etc\hosts' if windows?
|
|
42
|
+
|
|
43
|
+
'/etc/hosts' # fallback
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def home_directory
|
|
47
|
+
return ENV.fetch('USERPROFILE', nil) if windows?
|
|
48
|
+
|
|
49
|
+
Dir.home
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def detect_linux_package_manager
|
|
55
|
+
return :apt if apt_available?
|
|
56
|
+
return :yum if yum_available?
|
|
57
|
+
return :pacman if pacman_available?
|
|
58
|
+
|
|
59
|
+
:unknown
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def apt_available?
|
|
63
|
+
system('which apt > /dev/null 2>&1')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def yum_available?
|
|
67
|
+
system('which yum > /dev/null 2>&1') || system('which dnf > /dev/null 2>&1')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def pacman_available?
|
|
71
|
+
system('which pacman > /dev/null 2>&1')
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/stable/utils/prompts.rb
CHANGED
data/lib/stable.rb
CHANGED
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.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Danny Simfukwe
|
|
@@ -23,36 +23,9 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: 1.2.2
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '13.0'
|
|
33
|
-
type: :development
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '13.0'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: rspec
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '3.0'
|
|
47
|
-
type: :development
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '3.0'
|
|
54
|
-
description: 'Stable CLI: manage local Rails apps with automatic Caddy, HTTPS, and
|
|
55
|
-
simple start/stop commands.'
|
|
26
|
+
description: Stable is a cross-platform CLI tool to manage local Rails applications
|
|
27
|
+
with automatic Caddy setup, local trusted HTTPS certificates, and easy start/stop
|
|
28
|
+
functionality. Supports macOS, Linux, and Windows.
|
|
56
29
|
email:
|
|
57
30
|
- dannysimfukwe@gmail.com
|
|
58
31
|
executables:
|
|
@@ -93,6 +66,8 @@ files:
|
|
|
93
66
|
- lib/stable/services/ruby.rb
|
|
94
67
|
- lib/stable/services/setup_runner.rb
|
|
95
68
|
- lib/stable/system/shell.rb
|
|
69
|
+
- lib/stable/utils/package_manager.rb
|
|
70
|
+
- lib/stable/utils/platform.rb
|
|
96
71
|
- lib/stable/utils/prompts.rb
|
|
97
72
|
- lib/stable/validators/app_name.rb
|
|
98
73
|
homepage: https://github.com/dannysimfukwe/stable-rails
|