stable-cli-rails 0.8.0 → 0.8.2

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: ab8c647151029030baf0e96737f45f20cc6b067b952b032b98eac46e5e14e158
4
- data.tar.gz: 5db558cabc869e1f6d6684d26b1d7b08178679194d09ebdbbd42aa67d828aee7
3
+ metadata.gz: 0a7c350e5e5b2858a8a8ceb9e35eb9772c0e3416accf9f8e3546ba072ae59833
4
+ data.tar.gz: ed890e3547579a9361c473a8b884166a87d4454f1b89874e53510138f38e43f9
5
5
  SHA512:
6
- metadata.gz: fe68968ac73be6725262a224ceda502eaee576c2aa3a8822a2742577e9f5d6fc05f1c3092c6f7f1c0f011218aaa851949863e3473e8ea95de9e6fd91b94f31dc
7
- data.tar.gz: e333ac1da62ceb50a7a77ec83d1c145900f90b8888249c4307102bc1dfb0e3661e0bf97c1efe5a33be27b1aa409e639278ca91643989c3c2c0954d763c665533
6
+ metadata.gz: 9f86445108712b6eb9cb99a14f5da5df53e14d11d646d3ccc9525dc23efec767ae2a6a2f6cb68c0ac032f75928992dfb0913580900ebae6d2426660051f5191b
7
+ data.tar.gz: 2965495aa69ea21c4c19c7020222a90ee84214d05530a1b6175e9ce6689dda13d94a508cb708c9d2c2447f8b484b6afe127e7ae19c08efcf5dfefea247757d03
data/lib/stable/cli.rb CHANGED
@@ -75,6 +75,11 @@ module Stable
75
75
  Commands::Remove.new(name).call
76
76
  end
77
77
 
78
+ desc 'destroy NAME', 'Permanently delete a Rails app and all its files'
79
+ def destroy(name)
80
+ Commands::Destroy.new(name).call
81
+ end
82
+
78
83
  desc 'start NAME', 'Start a Rails app with its correct Ruby version'
79
84
  def start(name)
80
85
  Commands::Start.new(name).call
@@ -156,7 +161,7 @@ module Stable
156
161
  end
157
162
 
158
163
  def port_in_use?(port)
159
- system("lsof -i tcp:#{port} > /dev/null 2>&1")
164
+ Stable::Utils::Platform.port_in_use?(port)
160
165
  end
161
166
  end
162
167
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+
5
+ module Stable
6
+ module Commands
7
+ # Destroy command - permanently deletes a Rails application with confirmation
8
+ class Destroy
9
+ def initialize(name)
10
+ @name = name
11
+ end
12
+
13
+ def call
14
+ app = Services::AppRegistry.find(@name)
15
+ abort 'App not found' unless app
16
+
17
+ display_warning(app)
18
+ return unless confirm_destruction
19
+
20
+ puts "\nšŸ—‘ļø Destroying #{@name}..."
21
+ perform_destruction(app)
22
+ puts "āœ… Successfully destroyed #{@name}"
23
+ end
24
+
25
+ private
26
+
27
+ def display_warning(app)
28
+ puts "āš ļø WARNING: This will permanently delete the application '#{@name}'"
29
+ puts " Path: #{app[:path]}"
30
+ puts " Domain: #{app[:domain]}"
31
+ puts ' This action CANNOT be undone!'
32
+ puts ''
33
+ end
34
+
35
+ def confirm_destruction
36
+ print "Type '#{@name}' to confirm destruction: "
37
+ confirmation = $stdin.gets&.strip
38
+ puts ''
39
+
40
+ if confirmation == @name
41
+ true
42
+ else
43
+ puts "āŒ Destruction cancelled - confirmation didn't match"
44
+ false
45
+ end
46
+ end
47
+
48
+ def perform_destruction(app)
49
+ # Stop the app if running
50
+ Services::ProcessManager.stop(app)
51
+
52
+ # Remove from infrastructure
53
+ Services::HostsManager.remove(app[:domain])
54
+ Services::CaddyManager.remove(app[:domain])
55
+ Services::AppRegistry.remove(@name)
56
+
57
+ # Clean up RVM gemset
58
+ cleanup_rvm_gemset(app)
59
+
60
+ # Delete the project directory
61
+ delete_project_directory(app[:path])
62
+
63
+ # Reload Caddy
64
+ Services::CaddyManager.reload
65
+ end
66
+
67
+ def cleanup_rvm_gemset(app)
68
+ # Only clean up RVM gemsets on Unix-like systems (macOS/Linux)
69
+ # Windows uses different Ruby version managers
70
+ return unless Stable::Utils::Platform.unix?
71
+
72
+ ruby_version = app[:ruby]
73
+ # Handle different ruby version formats (e.g., "3.4.7", "ruby-3.4.7")
74
+ clean_ruby_version = ruby_version.to_s.sub(/^ruby-/, '')
75
+ gemset_name = "#{clean_ruby_version}@#{@name}"
76
+
77
+ puts " Cleaning up RVM gemset #{gemset_name}..."
78
+ begin
79
+ # Use system to run RVM command to delete the gemset
80
+ system("bash -lc 'source ~/.rvm/scripts/rvm && rvm gemset delete #{gemset_name} --force' 2>/dev/null || true")
81
+ puts " āœ… RVM gemset #{gemset_name} cleaned up"
82
+ rescue StandardError => e
83
+ puts " āš ļø Could not clean up RVM gemset #{gemset_name}: #{e.message}"
84
+ end
85
+ end
86
+
87
+ def delete_project_directory(path)
88
+ if File.exist?(path)
89
+ puts ' Deleting project directory...'
90
+ FileUtils.rm_rf(path)
91
+ else
92
+ puts ' Project directory not found (already deleted?)'
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../utils/platform'
4
+
3
5
  module Stable
4
6
  module Commands
5
7
  # List command - displays all registered applications
@@ -15,25 +17,22 @@ module Stable
15
17
  print_header
16
18
 
17
19
  apps.each do |app|
18
- puts format_row(app)
20
+ # Determine status based on whether the app is actually running (port check)
21
+ status = app_running?(app) ? 'running' : 'stopped'
22
+ puts format_row(app, status)
19
23
  end
20
24
  end
21
25
 
22
26
  private
23
27
 
24
- def print_header
25
- puts 'APP DOMAIN PORT RUBY STATUS '
26
- puts '-' * 78
27
- end
28
+ def app_running?(app)
29
+ return false unless app && app[:port]
28
30
 
29
- def format_row(app)
30
- status =
31
- if app[:started_at]
32
- 'running'
33
- else
34
- 'stopped'
35
- end
31
+ # Check if something is listening on the app's port (cross-platform)
32
+ Stable::Utils::Platform.port_in_use?(app[:port])
33
+ end
36
34
 
35
+ def format_row(app, status)
37
36
  format(
38
37
  '%<name>-18s %<domain>-26s %<port>-8s %<ruby>-10s %<status>-10s',
39
38
  name: app[:name],
@@ -43,6 +42,11 @@ module Stable
43
42
  status: status
44
43
  )
45
44
  end
45
+
46
+ def print_header
47
+ puts 'APP DOMAIN PORT RUBY STATUS '
48
+ puts '-' * 78
49
+ end
46
50
  end
47
51
  end
48
52
  end
@@ -66,6 +66,21 @@ module Stable
66
66
  def self.remove_app_config(app_name)
67
67
  config_file = Stable::Paths.app_config_file(app_name)
68
68
  FileUtils.rm_f(config_file)
69
+
70
+ # Also remove from legacy apps.yml file for backward compatibility
71
+ remove_from_legacy_file(app_name)
72
+ end
73
+
74
+ def self.remove_from_legacy_file(app_name)
75
+ legacy_file = Stable::Paths.apps_file
76
+ return unless File.exist?(legacy_file)
77
+
78
+ data = YAML.load_file(legacy_file) || []
79
+ filtered_data = data.reject { |entry| entry.is_a?(Hash) && entry['name'] == app_name }
80
+
81
+ return unless filtered_data != data
82
+
83
+ File.write(legacy_file, filtered_data.to_yaml)
69
84
  end
70
85
 
71
86
  def self.parse_config_file(config_file)
@@ -192,7 +192,7 @@ module Stable
192
192
  end
193
193
 
194
194
  def port_in_use?(port)
195
- system("lsof -i tcp:#{port} > /dev/null 2>&1")
195
+ Stable::Utils::Platform.port_in_use?(port)
196
196
  end
197
197
 
198
198
  def wait_for_port(port, timeout: 20)
@@ -55,7 +55,31 @@ module Stable
55
55
  return unless app
56
56
 
57
57
  updated_app = app.merge(attrs)
58
- Stable::Registry.save_app_config(name, updated_app)
58
+
59
+ # Check if this is a legacy app (from apps.yml) or new format app
60
+ config_file = Stable::Paths.app_config_file(name)
61
+ if File.exist?(config_file)
62
+ # New format: update individual config file
63
+ Stable::Registry.save_app_config(name, updated_app)
64
+ else
65
+ # Legacy format: update apps.yml file
66
+ update_legacy_app(name, updated_app)
67
+ end
68
+ end
69
+
70
+ def update_legacy_app(name, updated_app)
71
+ legacy_file = Stable::Paths.apps_file
72
+ return unless File.exist?(legacy_file)
73
+
74
+ data = YAML.load_file(legacy_file) || []
75
+ idx = data.find_index { |app| app['name'] == name || app[:name] == name }
76
+
77
+ return unless idx
78
+
79
+ # Convert symbols to strings for YAML compatibility
80
+ legacy_format = updated_app.transform_keys(&:to_s)
81
+ data[idx] = legacy_format
82
+ File.write(legacy_file, data.to_yaml)
59
83
  end
60
84
 
61
85
  def mark_stopped(name)
@@ -21,6 +21,14 @@ module Stable
21
21
 
22
22
  if app_running?(app)
23
23
  puts "#{@name} is already running on https://#{app[:domain]} (port #{port})"
24
+ # Update the registry with the correct PID if it's missing
25
+ if !app[:pid] || !app[:started_at]
26
+ rails_pid = find_rails_pid(port)
27
+ if rails_pid
28
+ AppRegistry.update(app[:name], started_at: Time.now.to_i, pid: rails_pid)
29
+ puts "Updated registry with correct PID (#{rails_pid})"
30
+ end
31
+ end
24
32
  return
25
33
  end
26
34
 
@@ -51,7 +59,9 @@ module Stable
51
59
 
52
60
  wait_for_port(port, timeout: 30)
53
61
 
54
- AppRegistry.update(app[:name], started_at: Time.now.to_i, pid: pid)
62
+ # Find the actual Rails process PID by checking what's listening on the port
63
+ rails_pid = find_rails_pid(port)
64
+ AppRegistry.update(app[:name], started_at: Time.now.to_i, pid: rails_pid)
55
65
 
56
66
  Stable::Services::CaddyManager.add_app(app[:name], skip_ssl: false)
57
67
  Stable::Services::CaddyManager.reload
@@ -69,7 +79,7 @@ module Stable
69
79
  end
70
80
 
71
81
  def port_in_use?(port)
72
- system("lsof -i tcp:#{port} > /dev/null 2>&1")
82
+ Stable::Utils::Platform.port_in_use?(port)
73
83
  end
74
84
 
75
85
  def wait_for_port(port, timeout: 20)
@@ -87,9 +97,20 @@ module Stable
87
97
  end
88
98
 
89
99
  def app_running?(app)
90
- return false unless app && app[:port]
100
+ return false unless app
101
+
102
+ # First check if we have PID info and if the process is alive
103
+ return ProcessManager.pid_alive?(app[:pid]) if app[:pid] && app[:started_at]
104
+
105
+ # Fallback to port checking if no PID info available
106
+ return false unless app[:port]
107
+
108
+ Stable::Utils::Platform.port_in_use?(app[:port])
109
+ end
91
110
 
92
- system("lsof -i tcp:#{app[:port]} -sTCP:LISTEN > /dev/null 2>&1")
111
+ def find_rails_pid(port)
112
+ pids = Stable::Utils::Platform.find_pids_by_port(port)
113
+ pids.first
93
114
  end
94
115
  end
95
116
  end
@@ -36,20 +36,27 @@ module Stable
36
36
 
37
37
  Process.detach(pid)
38
38
 
39
- AppRegistry.update(app[:name], started_at: Time.now.to_i, pid: pid)
39
+ # Wait a moment for Rails to start, then find the actual Rails PID
40
+ sleep 2
41
+ rails_pid = find_rails_pid(app[:port])
40
42
 
41
- pid
43
+ AppRegistry.update(app[:name], started_at: Time.now.to_i, pid: rails_pid || pid)
44
+
45
+ rails_pid || pid
42
46
  end
43
47
 
44
48
  def self.stop(app)
45
- pid = app[:pid]
46
- return unless pid
49
+ return unless app[:port]
47
50
 
48
- output = `lsof -i tcp:#{app[:port]} -t`.strip
49
- if output.empty?
51
+ pids = Stable::Utils::Platform.find_pids_by_port(app[:port])
52
+ if pids.empty?
50
53
  puts "No app running on port #{app[:port]}"
51
54
  else
52
- output.split("\n").each { |pid| Process.kill('TERM', pid.to_i) }
55
+ pids.each do |pid|
56
+ Process.kill('TERM', pid.to_i)
57
+ rescue StandardError
58
+ nil
59
+ end
53
60
  puts "Stopped #{app[:name]} on port #{app[:port]}"
54
61
  end
55
62
 
@@ -57,6 +64,43 @@ module Stable
57
64
  rescue Errno::ESRCH
58
65
  AppRegistry.update(app[:name], started_at: nil, pid: nil)
59
66
  end
67
+
68
+ # Check if a process with the given PID is still running
69
+ def self.pid_alive?(pid)
70
+ return false unless pid
71
+
72
+ # Use a cross-platform method to check if PID exists
73
+ if RUBY_PLATFORM =~ /mingw|mswin|win32/
74
+ # Windows: use tasklist
75
+ system("tasklist /FI \"PID eq #{pid}\" 2>NUL | find /I \"#{pid}\" >NUL")
76
+ else
77
+ # Unix-like systems: check /proc or use ps
78
+ begin
79
+ Process.kill(0, pid)
80
+ true
81
+ rescue Errno::ESRCH
82
+ false
83
+ rescue Errno::EPERM
84
+ # Process exists but we don't have permission to signal it
85
+ true
86
+ end
87
+ end
88
+ end
89
+
90
+ # Validate and clean up stale app statuses
91
+ def self.validate_app_statuses
92
+ apps = AppRegistry.all
93
+ apps.each do |app|
94
+ next unless app[:started_at] && app[:pid]
95
+
96
+ AppRegistry.update(app[:name], started_at: nil, pid: nil) unless pid_alive?(app[:pid])
97
+ end
98
+ end
99
+
100
+ def self.find_rails_pid(port)
101
+ pids = Stable::Utils::Platform.find_pids_by_port(port)
102
+ pids.first
103
+ end
60
104
  end
61
105
  end
62
106
  end
@@ -49,6 +49,36 @@ module Stable
49
49
  Dir.home
50
50
  end
51
51
 
52
+ def port_in_use?(port)
53
+ case current
54
+ when :macos, :linux
55
+ # Use lsof on Unix-like systems
56
+ system("lsof -i tcp:#{port} -sTCP:LISTEN > /dev/null 2>&1")
57
+ when :windows
58
+ # Use netstat on Windows
59
+ system("netstat -an | findstr :#{port} > nul 2>&1")
60
+ else
61
+ false
62
+ end
63
+ end
64
+
65
+ def find_pids_by_port(port)
66
+ case current
67
+ when :macos, :linux
68
+ # Use lsof to find PIDs listening on the port
69
+ output = `lsof -i tcp:#{port} -sTCP:LISTEN -t 2>/dev/null`.strip
70
+ return [] if output.empty?
71
+
72
+ output.split("\n").map(&:to_i)
73
+ when :windows
74
+ # On Windows, this is more complex. For now, return empty array
75
+ # Could potentially parse netstat output in the future
76
+ []
77
+ else
78
+ []
79
+ end
80
+ end
81
+
52
82
  private
53
83
 
54
84
  def detect_linux_package_manager
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.8.0
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danny Simfukwe
@@ -37,6 +37,7 @@ files:
37
37
  - lib/stable.rb
38
38
  - lib/stable/bootstrap.rb
39
39
  - lib/stable/cli.rb
40
+ - lib/stable/commands/destroy.rb
40
41
  - lib/stable/commands/doctor.rb
41
42
  - lib/stable/commands/list.rb
42
43
  - lib/stable/commands/new.rb