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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/stable/bootstrap.rb +1 -0
  3. data/lib/stable/cli.rb +1 -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 +11 -9
  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 +1 -0
  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 +1 -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: f092ba86af858aef49a2de7704a7039e3f25accafaf9028254469e9621c754ea
4
- data.tar.gz: 4ab71b60ae850c74508c790cf0fce5b14c2d12e0c8f2ec793a6b5e5234a95529
3
+ metadata.gz: ab8c647151029030baf0e96737f45f20cc6b067b952b032b98eac46e5e14e158
4
+ data.tar.gz: 5db558cabc869e1f6d6684d26b1d7b08178679194d09ebdbbd42aa67d828aee7
5
5
  SHA512:
6
- metadata.gz: 830108d826c9a63cdbaabf2c539b083795d7cf2772e1d8cea0c91d39dcefcaf06e792e1865793d7c7ab1da428b810228a36bbba34038555bcf20aefe03d2c295
7
- data.tar.gz: acdf43e4787cf9115eb29b4900f670cb9e2ce0a0fed29ba3f251cf40f00948897a3ed1c1edff3109053031c3ed6d238e4632d28f681fc591548635f9f03951ac
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?
@@ -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
@@ -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,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.expand_path(@name)
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
- System::Shell.run("bash -lc '#{rvm_cmd} rails new #{app_path} \
49
- --skip-importmap \
50
- --skip-hotwire \
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
- rails_version = `bash -lc 'cd #{app_path} && #{rvm_cmd} bundle exec rails runner "puts Rails.version"'`.strip.to_f
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
- 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
@@ -3,6 +3,7 @@
3
3
  module Stable
4
4
  module Services
5
5
  module Database
6
+ # Base class for database management services
6
7
  class Base
7
8
  def initialize(app_name:, app_path:, ruby:)
8
9
  @app_name = app_name
@@ -3,6 +3,7 @@
3
3
  module Stable
4
4
  module Services
5
5
  module Database
6
+ # MySQL database management service
6
7
  class MySQL < Base
7
8
  def setup
8
9
  creds = Stable::Utils::Prompts.mysql_root_credentials
@@ -3,6 +3,7 @@
3
3
  module Stable
4
4
  module Services
5
5
  module Database
6
+ # PostgreSQL database management service
6
7
  class Postgres < Base
7
8
  def setup
8
9
  System::Shell.run("createdb #{@database_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
- check('Homebrew', 'brew'),
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
- check('RVM', 'rvm'),
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
- ok = system('pgrep caddy > /dev/null 2>&1')
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 = File.expand_path('~/StableCaddy/certs')
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 = File.expand_path('~/StableCaddy/apps.yml')
50
- ok = File.exist?(path)
98
+ path = Stable::Paths.projects_dir
99
+ ok = Dir.exist?(path)
51
100
  {
52
- name: 'Apps registry',
101
+ name: 'Projects directory',
53
102
  ok: ok,
54
- message: ok ? nil : 'Missing apps registry. Run `stable setup`'
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
- HOSTS_FILE = '/etc/hosts'
9
+ def self.hosts_file
10
+ Stable::Utils::Platform.hosts_file
11
+ end
7
12
 
8
13
  def self.remove(domain)
9
- lines = File.read(HOSTS_FILE).lines
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
- File.write('/tmp/hosts', filtered.join)
15
- system("sudo mv /tmp/hosts #{HOSTS_FILE}")
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(HOSTS_FILE)
31
+ hosts = File.read(hosts_file)
21
32
  return if hosts.include?(domain)
22
33
 
23
34
  if Process.uid.zero?
24
- File.open(HOSTS_FILE, 'a') { |f| f.puts entry }
35
+ File.open(hosts_file, 'a') { |f| f.puts entry }
25
36
  else
26
- system(%(echo "#{entry}" | sudo tee -a #{HOSTS_FILE} > /dev/null))
37
+ system(%(echo "#{entry}" | sudo tee -a #{hosts_file} > /dev/null))
27
38
  end
28
39
  end
29
40
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Services
5
+ # Service for managing application processes
5
6
  class ProcessManager
6
7
  def self.start(target)
7
8
  app = target.is_a?(String) ? AppRegistry.fetch(target) : target
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Services
5
+ # Ruby version management utilities
5
6
  module Ruby
6
7
  def self.ensure_version(version)
7
8
  return if version.nil? || version.to_s.strip.empty?
@@ -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.apps_file
29
- File.write(path, {}.to_yaml) unless File.exist?(path)
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
- unless system('which brew > /dev/null')
45
- puts 'Homebrew is required. Install it first: https://brew.sh'
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
- system('brew install caddy')
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
- system('brew install mkcert nss')
64
+ install_mkcert(platform)
59
65
  end
60
66
 
61
- # Always ensure mkcert CA is installed
62
- system('mkcert -install')
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
- system('brew install postgresql')
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
- system('brew install mysql')
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module System
5
+ # Shell command execution utilities
5
6
  class Shell
6
7
  def self.run(cmd)
7
8
  puts "→ #{cmd}"
@@ -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
@@ -4,6 +4,7 @@ require 'io/console'
4
4
 
5
5
  module Stable
6
6
  module Utils
7
+ # User interaction utilities for prompting input
7
8
  class Prompts
8
9
  def self.mysql_root_credentials
9
10
  print 'Enter MySQL root username (default: root): '
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Stable
4
4
  module Validators
5
+ # Validates and normalizes Rails application names
5
6
  class AppName
6
7
  VALID_PATTERN = /\A[a-z0-9]+(-[a-z0-9]+)*\z/
7
8
  MAX_LENGTH = 63
data/lib/stable.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Main Stable CLI module - provides cross-platform Rails application management
3
4
  module Stable
4
5
  def self.root
5
6
  Paths.root
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.12
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
- - !ruby/object:Gem::Dependency
27
- name: rake
28
- requirement: !ruby/object:Gem::Requirement
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