stable-cli-rails 0.7.11 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/stable/bootstrap.rb +1 -0
  3. data/lib/stable/cli.rb +2 -5
  4. data/lib/stable/commands/doctor.rb +1 -0
  5. data/lib/stable/commands/list.rb +7 -6
  6. data/lib/stable/commands/new.rb +1 -0
  7. data/lib/stable/commands/remove.rb +1 -0
  8. data/lib/stable/commands/restart.rb +1 -0
  9. data/lib/stable/commands/setup.rb +1 -0
  10. data/lib/stable/commands/start.rb +1 -0
  11. data/lib/stable/commands/stop.rb +1 -0
  12. data/lib/stable/db_manager.rb +22 -7
  13. data/lib/stable/paths.rb +10 -1
  14. data/lib/stable/registry.rb +57 -7
  15. data/lib/stable/scanner.rb +1 -0
  16. data/lib/stable/services/app_creator.rb +118 -61
  17. data/lib/stable/services/app_registry.rb +7 -19
  18. data/lib/stable/services/app_remover.rb +1 -0
  19. data/lib/stable/services/app_restarter.rb +1 -0
  20. data/lib/stable/services/app_starter.rb +1 -0
  21. data/lib/stable/services/app_stopper.rb +1 -0
  22. data/lib/stable/services/caddy_manager.rb +1 -0
  23. data/lib/stable/services/database/base.rb +12 -2
  24. data/lib/stable/services/database/mysql.rb +1 -0
  25. data/lib/stable/services/database/postgres.rb +1 -0
  26. data/lib/stable/services/dependency_checker.rb +58 -9
  27. data/lib/stable/services/hosts_manager.rb +18 -7
  28. data/lib/stable/services/process_manager.rb +1 -0
  29. data/lib/stable/services/ruby.rb +10 -0
  30. data/lib/stable/services/setup_runner.rb +110 -16
  31. data/lib/stable/system/shell.rb +1 -0
  32. data/lib/stable/utils/package_manager.rb +68 -0
  33. data/lib/stable/utils/platform.rb +76 -0
  34. data/lib/stable/utils/prompts.rb +1 -0
  35. data/lib/stable/validators/app_name.rb +1 -0
  36. data/lib/stable.rb +1 -0
  37. metadata +6 -31
@@ -3,10 +3,12 @@
3
3
  module Stable
4
4
  module Services
5
5
  module Database
6
+ # Base class for database management services
6
7
  class Base
7
- def initialize(app_name:, app_path:)
8
+ def initialize(app_name:, app_path:, ruby:)
8
9
  @app_name = app_name
9
10
  @app_path = app_path
11
+ @ruby = ruby
10
12
  @database_name = @app_name
11
13
  .downcase
12
14
  .gsub(/[^a-z0-9_]/, '_')
@@ -16,7 +18,7 @@ module Stable
16
18
 
17
19
  def prepare
18
20
  System::Shell.run(
19
- "cd #{@app_path} && bundle exec rails db:prepare"
21
+ bash_rvm('bundle exec rails db:prepare')
20
22
  )
21
23
  end
22
24
 
@@ -41,6 +43,14 @@ module Stable
41
43
  def base_config(_creds)
42
44
  raise NotImplementedError
43
45
  end
46
+
47
+ def bash_rvm(cmd)
48
+ raise 'ruby not set' unless @ruby
49
+ raise 'app_name not set' unless @app_name
50
+ raise 'app_path not set' unless @app_path
51
+
52
+ "bash -lc 'cd #{@app_path} && source #{Ruby.rvm_script} && rvm #{@ruby}@#{@app_name} do #{cmd}'"
53
+ end
44
54
  end
45
55
  end
46
56
  end
@@ -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?
@@ -97,6 +98,15 @@ module Stable
97
98
  "rvm #{ruby} do"
98
99
  end
99
100
  end
101
+
102
+ def self.clean_rvm_exec(ruby, gemset, cmd)
103
+ <<~CMD
104
+ bash -lc '
105
+ source #{rvm_script};
106
+ rvm #{ruby}@#{gemset} --create do #{cmd}
107
+ '
108
+ CMD
109
+ end
100
110
  end
101
111
  end
102
112
  end
@@ -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.11
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