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 +4 -4
- data/lib/stable/cli.rb +6 -1
- data/lib/stable/commands/destroy.rb +97 -0
- data/lib/stable/commands/list.rb +16 -12
- data/lib/stable/registry.rb +15 -0
- data/lib/stable/services/app_creator.rb +1 -1
- data/lib/stable/services/app_registry.rb +25 -1
- data/lib/stable/services/app_starter.rb +25 -4
- data/lib/stable/services/process_manager.rb +51 -7
- data/lib/stable/utils/platform.rb +30 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a7c350e5e5b2858a8a8ceb9e35eb9772c0e3416accf9f8e3546ba072ae59833
|
|
4
|
+
data.tar.gz: ed890e3547579a9361c473a8b884166a87d4454f1b89874e53510138f38e43f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
data/lib/stable/commands/list.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
puts '-' * 78
|
|
27
|
-
end
|
|
28
|
+
def app_running?(app)
|
|
29
|
+
return false unless app && app[:port]
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
data/lib/stable/registry.rb
CHANGED
|
@@ -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)
|
|
@@ -55,7 +55,31 @@ module Stable
|
|
|
55
55
|
return unless app
|
|
56
56
|
|
|
57
57
|
updated_app = app.merge(attrs)
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
return unless pid
|
|
49
|
+
return unless app[:port]
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
if
|
|
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
|
-
|
|
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.
|
|
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
|