stable-cli-rails 0.1.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 +7 -0
- data/README.md +184 -0
- data/bin/stable +4 -0
- data/lib/stable/bootstrap.rb +36 -0
- data/lib/stable/cli.rb +545 -0
- data/lib/stable/paths.rb +23 -0
- data/lib/stable/registry.rb +19 -0
- data/lib/stable/scanner.rb +8 -0
- data/lib/stable.rb +14 -0
- metadata +121 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cb7161cbe3a5e9ed3d651e059f0884fd065cf1281a13baf1ee7f518e47ce66c4
|
|
4
|
+
data.tar.gz: 882e21ccd55072e66bf5f0e4dac8b023b41da126667d17de930876d69827d536
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4f093a5af1f22b2212280b57bc69afd934b44525378a707c5c0bb399b3645b4ee936aea242e316628015930b3ab4ee3ea0f99bd798dc1aa8514ef98aea775c30
|
|
7
|
+
data.tar.gz: c87973bf27d5f0d297ebb7104d0197d5432ce360f278d0a5711fe639c928ee58a363a16f1bf07857e79c79ded85849d6721416be00e549019ae522d171a0fe57
|
data/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Stable CLI (macOS)
|
|
2
|
+
|
|
3
|
+
Stable is a CLI tool to manage local Rails applications with automatic Caddy setup on macOS, local trusted HTTPS certificates, and easy start/stop functionality.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Add and remove Rails apps.
|
|
8
|
+
- Automatically generate and manage local HTTPS certificates using `mkcert`.
|
|
9
|
+
- Automatically update `/etc/hosts` for `.test` domains.
|
|
10
|
+
- Start Rails apps with integrated Caddy reverse proxy.
|
|
11
|
+
- Reload Caddy after adding/removing apps.
|
|
12
|
+
- List all registered apps.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### From source
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Clone the repository
|
|
20
|
+
git clone git@github.com:dannysimfukwe/stable-rails.git
|
|
21
|
+
cd stable-rails
|
|
22
|
+
|
|
23
|
+
# Install dependencies
|
|
24
|
+
bundle install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### As a gem
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
gem install stable
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
Initialize Caddy home and required directories:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
stable setup
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This will create:
|
|
42
|
+
- `~/StableCaddy/` for Caddy configuration.
|
|
43
|
+
- `~/StableCaddy/certs` for generated certificates.
|
|
44
|
+
- `~/StableCaddy/Caddyfile` for Caddy configuration.
|
|
45
|
+
|
|
46
|
+
## CLI Commands
|
|
47
|
+
|
|
48
|
+
### List apps
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
stable list
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Lists all registered apps and their domains.
|
|
55
|
+
|
|
56
|
+
### Add a Rails app
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
stable add /path/to/rails_app
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This will:
|
|
63
|
+
- Register the app.
|
|
64
|
+
- Add a `/etc/hosts` entry.
|
|
65
|
+
- Generate local trusted HTTPS certificates.
|
|
66
|
+
- Add a Caddy reverse proxy block.
|
|
67
|
+
- Reload Caddy.
|
|
68
|
+
|
|
69
|
+
### Remove a Rails app
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
stable remove app_name
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This will:
|
|
76
|
+
- Remove the app from registry.
|
|
77
|
+
- Remove `/etc/hosts` entry.
|
|
78
|
+
- Remove the Caddy reverse proxy block.
|
|
79
|
+
- Reload Caddy.
|
|
80
|
+
|
|
81
|
+
### Start an app
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
rvmsudo stable start app_name
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Starts the Rails server on the assigned port and ensures Caddy is running with the proper reverse proxy. Rails logs can be viewed in your terminal.
|
|
88
|
+
|
|
89
|
+
### Stop an app
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
stable stop app_name
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Stops the Rails server running on the assigned port.
|
|
96
|
+
|
|
97
|
+
### Secure an app manually
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
rvmsudo stable secure app_name.test
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Generates or updates trusted local HTTPS certificates and reloads Caddy.
|
|
104
|
+
|
|
105
|
+
### Reload Caddy
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
stable caddy reload
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Reloads Caddy configuration after changes.
|
|
112
|
+
|
|
113
|
+
### Health check
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
stable doctor
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Checks the environment, RVM/Ruby, Caddy, mkcert, and app readiness.
|
|
120
|
+
|
|
121
|
+
### Upgrade Ruby for an app
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
stable upgrade-ruby myapp 3.4.4
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Upgrades the Ruby version for a specific app, updating `.ruby-version` and ensuring gemset compatibility.
|
|
128
|
+
|
|
129
|
+
### Create a new Rails app
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
stable new myapp [--ruby 3.4.4] [--rails 7.0.7.1] [--skip-ssl]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Creates a new Rails app, generates `.ruby-version`, installs Rails, adds the app to Stable, and optionally secures it with HTTPS.
|
|
136
|
+
|
|
137
|
+
## Paths
|
|
138
|
+
|
|
139
|
+
- Caddy home: `~/StableCaddy`
|
|
140
|
+
- Caddyfile: `~/StableCaddy/Caddyfile`
|
|
141
|
+
- Certificates: `~/StableCaddy/certs`
|
|
142
|
+
- Registered apps: `~/StableCaddy/apps.yml`
|
|
143
|
+
|
|
144
|
+
## Dependencies
|
|
145
|
+
|
|
146
|
+
- Homebrew
|
|
147
|
+
- Caddy
|
|
148
|
+
- mkcert
|
|
149
|
+
- RVM (or rbenv fallback)
|
|
150
|
+
|
|
151
|
+
`ensure_dependencies!` will install missing dependencies automatically.
|
|
152
|
+
|
|
153
|
+
## Known Issues
|
|
154
|
+
|
|
155
|
+
- Sometimes you may see:
|
|
156
|
+
```
|
|
157
|
+
TCPSocket#initialize: Connection refused - connect(2) for "127.0.0.1" port 300.. (Errno::ECONNREFUSED)
|
|
158
|
+
```
|
|
159
|
+
This usually disappears after a few seconds when Caddy reloads. If it persists, run:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
rvmsudo stable secure myapp.test
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- Some commands may need to be run consecutively for proper setup:
|
|
166
|
+
```bash
|
|
167
|
+
stable setup
|
|
168
|
+
rvmsudo stable add myapp
|
|
169
|
+
rvmsudo stable secure myapp.test
|
|
170
|
+
stable start myapp
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- PATH warnings from RVM may appear on the first run. Make sure your shell is properly configured for RVM.
|
|
174
|
+
|
|
175
|
+
## Notes
|
|
176
|
+
|
|
177
|
+
- Make sure to run `stable setup` initially.
|
|
178
|
+
- Requires `sudo` to modify `/etc/hosts`.
|
|
179
|
+
- Rails apps are started on ports assigned by Stable (default 3000+).
|
|
180
|
+
- Domains are automatically suffixed with `.test`.
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT License
|
data/bin/stable
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Stable
|
|
4
|
+
module Bootstrap
|
|
5
|
+
def self.run!
|
|
6
|
+
FileUtils.mkdir_p(Paths.root)
|
|
7
|
+
FileUtils.mkdir_p(Paths.caddy_dir)
|
|
8
|
+
FileUtils.mkdir_p(Paths.certs_dir)
|
|
9
|
+
|
|
10
|
+
unless File.exist?(Paths.apps_file)
|
|
11
|
+
File.write(Paths.apps_file, "--- []\n")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
unless File.exist?(Paths.caddyfile)
|
|
15
|
+
File.write(Paths.caddyfile, "")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
disable_rvm_autolibs!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.disable_rvm_autolibs!
|
|
22
|
+
return unless system("which rvm > /dev/null")
|
|
23
|
+
|
|
24
|
+
# Only run once
|
|
25
|
+
marker = File.join(Paths.root, ".rvm_autolibs_disabled")
|
|
26
|
+
return if File.exist?(marker)
|
|
27
|
+
|
|
28
|
+
puts "Configuring RVM (disabling autolibs)..."
|
|
29
|
+
|
|
30
|
+
system("bash -lc 'rvm autolibs disable'")
|
|
31
|
+
system("bash -lc 'echo rvm_silence_path_mismatch_check_flag=1 >> ~/.rvmrc'")
|
|
32
|
+
|
|
33
|
+
FileUtils.touch(marker)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/stable/cli.rb
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require "etc"
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "scanner"
|
|
5
|
+
require_relative "registry"
|
|
6
|
+
|
|
7
|
+
module Stable
|
|
8
|
+
class CLI < Thor
|
|
9
|
+
|
|
10
|
+
HOSTS_FILE = "/etc/hosts".freeze
|
|
11
|
+
|
|
12
|
+
def initialize(*)
|
|
13
|
+
super
|
|
14
|
+
Stable::Bootstrap.run!
|
|
15
|
+
ensure_dependencies!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.exit_on_failure?
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "new NAME", "Create, secure, and run a new Rails app"
|
|
23
|
+
method_option :ruby, type: :string, desc: "Ruby version (defaults to current Ruby)"
|
|
24
|
+
method_option :rails, type: :string, desc: "Rails version to install (optional)"
|
|
25
|
+
method_option :port, type: :numeric, desc: "Port to run Rails app on"
|
|
26
|
+
method_option :skip_ssl, type: :boolean, default: false, desc: "Skip HTTPS setup"
|
|
27
|
+
def new(name, ruby: RUBY_VERSION, rails: nil, port: nil)
|
|
28
|
+
port ||= next_free_port
|
|
29
|
+
app_path = File.expand_path(name)
|
|
30
|
+
|
|
31
|
+
abort "Folder already exists: #{app_path}" if File.exist?(app_path)
|
|
32
|
+
|
|
33
|
+
# --- Ensure RVM and Ruby ---
|
|
34
|
+
ensure_rvm!
|
|
35
|
+
puts "Using Ruby #{ruby} with RVM gemset #{name}..."
|
|
36
|
+
system("bash -lc 'rvm #{ruby}@#{name} --create do true'") or abort("Failed to create RVM gemset")
|
|
37
|
+
|
|
38
|
+
# --- Install Rails in gemset if needed ---
|
|
39
|
+
rails_version = rails || "latest"
|
|
40
|
+
rails_check = system("bash -lc 'rvm #{ruby}@#{name} do gem list -i rails#{rails ? " -v #{rails}" : ""}'")
|
|
41
|
+
unless rails_check
|
|
42
|
+
puts "Installing Rails #{rails_version} in gemset..."
|
|
43
|
+
system("bash -lc 'rvm #{ruby}@#{name} do gem install rails #{rails ? "-v #{rails}" : ""}'") or abort("Failed to install Rails")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# --- Create Rails app ---
|
|
47
|
+
puts "Creating Rails app #{name} (Ruby #{ruby})..."
|
|
48
|
+
system("bash -lc 'rvm #{ruby}@#{name} do rails new #{app_path}'") or abort("Rails app creation failed")
|
|
49
|
+
|
|
50
|
+
# --- Add .ruby-version and .ruby-gemset ---
|
|
51
|
+
Dir.chdir(app_path) do
|
|
52
|
+
File.write(".ruby-version", "#{ruby}\n")
|
|
53
|
+
File.write(".ruby-gemset", "#{name}\n")
|
|
54
|
+
|
|
55
|
+
# --- Install gems inside gemset ---
|
|
56
|
+
puts "Running bundle install..."
|
|
57
|
+
system("bash -lc 'rvm #{ruby}@#{name} do bundle install --jobs=4 --retry=3'") or abort("bundle install failed")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# --- Add app to registry ---
|
|
61
|
+
domain = "#{name}.test"
|
|
62
|
+
apps = Registry.apps
|
|
63
|
+
apps << { name: name, path: app_path, domain: domain, port: port, ruby: ruby }
|
|
64
|
+
Registry.save(apps)
|
|
65
|
+
|
|
66
|
+
# --- Host entry & certificate ---
|
|
67
|
+
add_host_entry(domain)
|
|
68
|
+
generate_cert(domain) unless options[:skip_ssl]
|
|
69
|
+
update_caddyfile(domain, port)
|
|
70
|
+
ensure_caddy_running!
|
|
71
|
+
caddy_reload
|
|
72
|
+
|
|
73
|
+
# --- Start Rails server ---
|
|
74
|
+
puts "Starting Rails server for #{name} on port #{port}..."
|
|
75
|
+
log_file = File.join(app_path, "log", "stable.log")
|
|
76
|
+
FileUtils.mkdir_p(File.dirname(log_file))
|
|
77
|
+
pid = spawn("bash -lc 'rvm #{ruby}@#{name} do cd #{app_path} && bundle exec rails s -p #{port} >> #{log_file} 2>&1'")
|
|
78
|
+
Process.detach(pid)
|
|
79
|
+
|
|
80
|
+
wait_for_port(port)
|
|
81
|
+
puts "✔ #{name} running at https://#{domain}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
desc "list", "List detected apps"
|
|
86
|
+
def list
|
|
87
|
+
apps = Registry.apps
|
|
88
|
+
if apps.empty?
|
|
89
|
+
puts "No apps found."
|
|
90
|
+
else
|
|
91
|
+
apps.each do |app|
|
|
92
|
+
puts "#{app[:name]} -> https://#{app[:domain]}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
desc "add FOLDER", "Add a Rails app folder"
|
|
98
|
+
def add(folder)
|
|
99
|
+
folder = File.expand_path(folder)
|
|
100
|
+
unless File.exist?(File.join(folder, "config", "application.rb"))
|
|
101
|
+
puts "Not a Rails app: #{folder}"
|
|
102
|
+
return
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
apps = Registry.apps
|
|
106
|
+
name = File.basename(folder)
|
|
107
|
+
domain = "#{name}.test"
|
|
108
|
+
|
|
109
|
+
if apps.any? { |a| a[:path] == folder }
|
|
110
|
+
puts "App already exists: #{name}"
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
port = next_free_port
|
|
115
|
+
ruby = detect_ruby_version(folder)
|
|
116
|
+
|
|
117
|
+
apps << { name: name, path: folder, domain: domain, port: port, ruby: ruby }
|
|
118
|
+
Registry.save(apps)
|
|
119
|
+
puts "Added #{name} -> https://#{domain} (port #{port})"
|
|
120
|
+
|
|
121
|
+
add_host_entry(domain)
|
|
122
|
+
generate_cert(domain)
|
|
123
|
+
update_caddyfile(domain, port)
|
|
124
|
+
caddy_reload
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
desc "remove NAME", "Remove an app by name"
|
|
128
|
+
def remove(name)
|
|
129
|
+
apps = Registry.apps
|
|
130
|
+
app = apps.find { |a| a[:name] == name }
|
|
131
|
+
if app.nil?
|
|
132
|
+
puts "No app found with name #{name}"
|
|
133
|
+
return
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
new_apps = apps.reject { |a| a[:name] == name }
|
|
137
|
+
Registry.save(new_apps)
|
|
138
|
+
puts "Removed #{name}"
|
|
139
|
+
|
|
140
|
+
remove_host_entry(app[:domain])
|
|
141
|
+
remove_caddy_entry(app[:domain])
|
|
142
|
+
caddy_reload
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
desc "start NAME", "Start a Rails app with its correct Ruby version"
|
|
146
|
+
def start(name)
|
|
147
|
+
app = Registry.apps.find { |a| a[:name] == name }
|
|
148
|
+
unless app
|
|
149
|
+
puts "No app found with name #{name}"
|
|
150
|
+
return
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
port = app[:port] || next_free_port
|
|
154
|
+
ruby = app[:ruby]
|
|
155
|
+
|
|
156
|
+
puts "Starting #{name} on port #{port}#{ruby ? " (Ruby #{ruby})" : ""}..."
|
|
157
|
+
|
|
158
|
+
log_file = File.join(app[:path], "log", "stable.log")
|
|
159
|
+
FileUtils.mkdir_p(File.dirname(log_file))
|
|
160
|
+
|
|
161
|
+
ruby_exec =
|
|
162
|
+
if ruby
|
|
163
|
+
if rvm_available?
|
|
164
|
+
ensure_rvm_ruby!(ruby)
|
|
165
|
+
"rvm #{ruby}@#{name} do"
|
|
166
|
+
elsif rbenv_available?
|
|
167
|
+
ensure_rbenv_ruby!(ruby)
|
|
168
|
+
"RBENV_VERSION=#{ruby}"
|
|
169
|
+
else
|
|
170
|
+
puts "No Ruby version manager found (rvm or rbenv)"
|
|
171
|
+
return
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
cmd = <<~CMD
|
|
176
|
+
cd #{app[:path]} &&
|
|
177
|
+
#{ruby_exec} bundle exec rails s -p #{port}
|
|
178
|
+
CMD
|
|
179
|
+
|
|
180
|
+
pid = spawn(
|
|
181
|
+
"bash",
|
|
182
|
+
"-lc",
|
|
183
|
+
cmd,
|
|
184
|
+
out: log_file,
|
|
185
|
+
err: log_file
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
Process.detach(pid)
|
|
189
|
+
|
|
190
|
+
generate_cert(app[:domain])
|
|
191
|
+
update_caddyfile(app[:domain], port)
|
|
192
|
+
wait_for_port(port)
|
|
193
|
+
caddy_reload
|
|
194
|
+
|
|
195
|
+
puts "#{name} started on https://#{app[:domain]}"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
desc "stop NAME", "Stop a Rails app (default port 3000)"
|
|
199
|
+
def stop(name)
|
|
200
|
+
app = Registry.apps.find { |a| a[:name] == name }
|
|
201
|
+
|
|
202
|
+
output = `lsof -i tcp:#{app[:port]} -t`.strip
|
|
203
|
+
if output.empty?
|
|
204
|
+
puts "No app running on port #{app[:port]}"
|
|
205
|
+
else
|
|
206
|
+
output.split("\n").each { |pid| Process.kill("TERM", pid.to_i) }
|
|
207
|
+
puts "Stopped #{name}"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
desc "setup", "Sets up Caddy and local trusted certificates"
|
|
212
|
+
def setup
|
|
213
|
+
FileUtils.mkdir_p(Stable::Paths.root)
|
|
214
|
+
File.write(Stable::Paths.caddyfile, "") unless File.exist?(Stable::Paths.caddyfile)
|
|
215
|
+
ensure_caddy_running!
|
|
216
|
+
puts "Caddy home initialized at #{Stable::Paths.root}"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
desc "caddy reload", "Reloads Caddy after adding/removing apps"
|
|
220
|
+
def caddy_reload
|
|
221
|
+
if system("which caddy > /dev/null")
|
|
222
|
+
system("caddy reload --config #{Stable::Paths.caddyfile}")
|
|
223
|
+
puts "Caddy reloaded"
|
|
224
|
+
else
|
|
225
|
+
puts "Caddy not found. Install Caddy first."
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
desc "secure DOMAIN", "Generate trusted local HTTPS cert for a specific folder/domain"
|
|
230
|
+
def secure(domain)
|
|
231
|
+
app = Registry.apps.find { |a| a[:domain] == domain }
|
|
232
|
+
unless app
|
|
233
|
+
puts "No app found with domain #{domain}"
|
|
234
|
+
return
|
|
235
|
+
end
|
|
236
|
+
secure_app(domain, app[:path], app[:port])
|
|
237
|
+
caddy_reload
|
|
238
|
+
puts "Secured https://#{domain}"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
desc "doctor", "Check Stable system health"
|
|
243
|
+
def doctor
|
|
244
|
+
puts "Stable doctor\n\n"
|
|
245
|
+
|
|
246
|
+
puts "Ruby version: #{RUBY_VERSION}"
|
|
247
|
+
puts "RVM: #{rvm_available? ? "yes" : "no"}"
|
|
248
|
+
puts "rbenv: #{rbenv_available? ? "yes" : "no"}"
|
|
249
|
+
puts "Caddy: #{system("which caddy > /dev/null") ? "yes" : "no"}"
|
|
250
|
+
puts "mkcert: #{system("which mkcert > /dev/null") ? "yes" : "no"}"
|
|
251
|
+
|
|
252
|
+
Registry.apps.each do |app|
|
|
253
|
+
status = port_in_use?(app[:port]) ? "running" : "stopped"
|
|
254
|
+
puts "#{app[:name]} → Ruby #{app[:ruby] || "default"} (#{status})"
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
desc "upgrade-ruby NAME VERSION", "Upgrade Ruby for an app"
|
|
259
|
+
def upgrade_ruby(name, version)
|
|
260
|
+
app = Registry.apps.find { |a| a[:name] == name }
|
|
261
|
+
unless app
|
|
262
|
+
puts "No app named #{name}"
|
|
263
|
+
return
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
if rvm_available?
|
|
267
|
+
system("bash -lc 'rvm install #{version}'")
|
|
268
|
+
elsif rbenv_available?
|
|
269
|
+
system("rbenv install #{version}")
|
|
270
|
+
else
|
|
271
|
+
puts "No Ruby version manager found"
|
|
272
|
+
return
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
File.write(File.join(app[:path], ".ruby-version"), version)
|
|
276
|
+
app[:ruby] = version
|
|
277
|
+
Registry.save(Registry.apps)
|
|
278
|
+
|
|
279
|
+
puts "#{name} now uses Ruby #{version}"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
private
|
|
283
|
+
|
|
284
|
+
def add_host_entry(domain)
|
|
285
|
+
entry = "127.0.0.1\t#{domain}"
|
|
286
|
+
hosts = File.read(HOSTS_FILE)
|
|
287
|
+
unless hosts.include?(domain)
|
|
288
|
+
puts "Adding #{domain} to #{HOSTS_FILE}..."
|
|
289
|
+
File.open(HOSTS_FILE, "a") { |f| f.puts entry }
|
|
290
|
+
system("dscacheutil -flushcache; sudo killall -HUP mDNSResponder")
|
|
291
|
+
end
|
|
292
|
+
rescue Errno::EACCES
|
|
293
|
+
ensure_hosts_entry(domain)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def remove_host_entry(domain)
|
|
297
|
+
begin
|
|
298
|
+
hosts = File.read(HOSTS_FILE)
|
|
299
|
+
new_hosts = hosts.lines.reject { |line| line.include?(domain) }.join
|
|
300
|
+
File.write(HOSTS_FILE, new_hosts)
|
|
301
|
+
system("dscacheutil -flushcache; sudo killall -HUP mDNSResponder")
|
|
302
|
+
rescue Errno::EACCES
|
|
303
|
+
puts "Permission denied updating #{HOSTS_FILE}. Run 'sudo stable remove #{domain}' to remove hosts entry."
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def ensure_hosts_entry(domain)
|
|
308
|
+
entry = "127.0.0.1\t#{domain}"
|
|
309
|
+
|
|
310
|
+
hosts = File.read(HOSTS_FILE)
|
|
311
|
+
return if hosts.include?(domain)
|
|
312
|
+
|
|
313
|
+
if Process.uid.zero?
|
|
314
|
+
File.open(HOSTS_FILE, "a") { |f| f.puts entry }
|
|
315
|
+
else
|
|
316
|
+
system(%(echo "#{entry}" | sudo tee -a #{HOSTS_FILE} > /dev/null))
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
system("dscacheutil -flushcache; sudo killall -HUP mDNSResponder")
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def secure_app(domain, _folder, port)
|
|
323
|
+
ensure_certs_dir!
|
|
324
|
+
|
|
325
|
+
cert_path = File.join(Stable::Paths.certs_dir, "#{domain}.pem")
|
|
326
|
+
key_path = File.join(Stable::Paths.certs_dir, "#{domain}-key.pem")
|
|
327
|
+
|
|
328
|
+
# Generate certificates if missing
|
|
329
|
+
if system("which mkcert > /dev/null")
|
|
330
|
+
unless File.exist?(cert_path) && File.exist?(key_path)
|
|
331
|
+
system("mkcert -cert-file #{cert_path} -key-file #{key_path} #{domain}")
|
|
332
|
+
end
|
|
333
|
+
else
|
|
334
|
+
puts "mkcert not found. Please install mkcert."
|
|
335
|
+
return
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Auto-add Caddy block if not already in Caddyfile
|
|
339
|
+
add_caddy_block(domain, cert_path, key_path, port)
|
|
340
|
+
caddy_reload
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def add_caddy_block(domain, cert, key, port)
|
|
344
|
+
caddyfile = Stable::Paths.caddyfile
|
|
345
|
+
FileUtils.touch(caddyfile) unless File.exist?(caddyfile)
|
|
346
|
+
content = File.read(caddyfile)
|
|
347
|
+
|
|
348
|
+
return if content.include?(domain) # don't duplicate
|
|
349
|
+
|
|
350
|
+
block = <<~CADDY
|
|
351
|
+
|
|
352
|
+
https://#{domain} {
|
|
353
|
+
reverse_proxy 127.0.0.1:#{port}
|
|
354
|
+
tls #{cert} #{key}
|
|
355
|
+
}
|
|
356
|
+
CADDY
|
|
357
|
+
|
|
358
|
+
File.write(caddyfile, content + block)
|
|
359
|
+
system("caddy fmt --overwrite #{caddyfile}")
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# Remove Caddyfile entry for the domain
|
|
364
|
+
def remove_caddy_entry(domain)
|
|
365
|
+
return unless File.exist?(Stable::Paths.caddyfile)
|
|
366
|
+
content = File.read(Stable::Paths.caddyfile)
|
|
367
|
+
# Remove block starting with https://<domain> { ... }
|
|
368
|
+
new_content = content.gsub(/https:\/\/#{Regexp.escape(domain)}\s*\{[^\}]*\}/m, "")
|
|
369
|
+
File.write(Stable::Paths.caddyfile, new_content)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def ensure_dependencies!
|
|
373
|
+
unless system("which brew > /dev/null")
|
|
374
|
+
puts "Homebrew is required. Install it first: https://brew.sh"
|
|
375
|
+
exit 1
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
unless system("which caddy > /dev/null")
|
|
379
|
+
puts "Installing Caddy..."
|
|
380
|
+
system("brew install caddy")
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
unless system("which mkcert > /dev/null")
|
|
384
|
+
puts "Installing mkcert..."
|
|
385
|
+
system("brew install mkcert nss")
|
|
386
|
+
system("mkcert -install")
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
def ensure_caddy_running!
|
|
390
|
+
api_port = 2019
|
|
391
|
+
|
|
392
|
+
# Check if Caddy API is reachable
|
|
393
|
+
require 'socket'
|
|
394
|
+
begin
|
|
395
|
+
TCPSocket.new('127.0.0.1', api_port).close
|
|
396
|
+
puts "Caddy already running."
|
|
397
|
+
rescue Errno::ECONNREFUSED
|
|
398
|
+
puts "Starting Caddy in background..."
|
|
399
|
+
system("caddy run --config #{Stable::Paths.caddyfile} --adapter caddyfile --watch --resume &")
|
|
400
|
+
sleep 3
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def next_free_port
|
|
406
|
+
used_ports = Registry.apps.map { |a| a[:port] }
|
|
407
|
+
port = 3000
|
|
408
|
+
port += 1 while used_ports.include?(port) || port_in_use?(port)
|
|
409
|
+
port
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def port_in_use?(port)
|
|
413
|
+
system("lsof -i tcp:#{port} > /dev/null 2>&1")
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def generate_cert(domain)
|
|
417
|
+
cert_path = File.join(Stable::Paths.certs_dir, "#{domain}.pem")
|
|
418
|
+
key_path = File.join(Stable::Paths.certs_dir, "#{domain}-key.pem")
|
|
419
|
+
FileUtils.mkdir_p(Stable::Paths.certs_dir)
|
|
420
|
+
|
|
421
|
+
unless File.exist?(cert_path) && File.exist?(key_path)
|
|
422
|
+
if system("which mkcert > /dev/null")
|
|
423
|
+
system("mkcert -cert-file #{cert_path} -key-file #{key_path} #{domain}")
|
|
424
|
+
else
|
|
425
|
+
puts "mkcert not found. Please install mkcert."
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def update_caddyfile(domain, port)
|
|
431
|
+
caddyfile = Stable::Paths.caddyfile
|
|
432
|
+
FileUtils.touch(caddyfile) unless File.exist?(caddyfile)
|
|
433
|
+
content = File.read(caddyfile)
|
|
434
|
+
|
|
435
|
+
# remove existing block for domain
|
|
436
|
+
content.gsub!(/https:\/\/#{Regexp.escape(domain)}\s*\{[^\}]*\}/m, "")
|
|
437
|
+
|
|
438
|
+
# add new block
|
|
439
|
+
cert_path = File.join(Stable::Paths.certs_dir, "#{domain}.pem")
|
|
440
|
+
key_path = File.join(Stable::Paths.certs_dir, "#{domain}-key.pem")
|
|
441
|
+
block = <<~CADDY
|
|
442
|
+
|
|
443
|
+
https://#{domain} {
|
|
444
|
+
reverse_proxy 127.0.0.1:#{port}
|
|
445
|
+
tls #{cert_path} #{key_path}
|
|
446
|
+
}
|
|
447
|
+
CADDY
|
|
448
|
+
|
|
449
|
+
File.write(caddyfile, content + block)
|
|
450
|
+
system("caddy fmt --overwrite #{caddyfile}")
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def ensure_certs_dir!
|
|
454
|
+
certs_dir = Stable::Paths.certs_dir
|
|
455
|
+
FileUtils.mkdir_p(certs_dir)
|
|
456
|
+
|
|
457
|
+
begin
|
|
458
|
+
FileUtils.chown_R(Etc.getlogin, nil, certs_dir)
|
|
459
|
+
rescue => e
|
|
460
|
+
puts "Could not change ownership: #{e.message}"
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Restrict permissions for security
|
|
464
|
+
Dir.glob("#{certs_dir}/*.pem").each do |pem|
|
|
465
|
+
FileUtils.chmod(0600, pem)
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def wait_for_port(port, timeout: 5)
|
|
470
|
+
require 'socket'
|
|
471
|
+
start_time = Time.now
|
|
472
|
+
loop do
|
|
473
|
+
begin
|
|
474
|
+
TCPSocket.new('127.0.0.1', port).close
|
|
475
|
+
break
|
|
476
|
+
rescue Errno::ECONNREFUSED
|
|
477
|
+
raise "Timeout waiting for port #{port}" if Time.now - start_time > timeout
|
|
478
|
+
sleep 0.1
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def ensure_rvm!
|
|
484
|
+
return if system("which rvm > /dev/null")
|
|
485
|
+
|
|
486
|
+
puts "RVM not found. Installing RVM..."
|
|
487
|
+
|
|
488
|
+
install_cmd = <<~CMD
|
|
489
|
+
curl -sSL https://get.rvm.io | bash -s stable
|
|
490
|
+
CMD
|
|
491
|
+
|
|
492
|
+
unless system(install_cmd)
|
|
493
|
+
abort "RVM installation failed"
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Load RVM into current process
|
|
497
|
+
rvm_script = File.expand_path("~/.rvm/scripts/rvm")
|
|
498
|
+
unless File.exist?(rvm_script)
|
|
499
|
+
abort "RVM installed but could not be loaded"
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
ENV["PATH"] = "#{File.expand_path('~/.rvm/bin')}:#{ENV['PATH']}"
|
|
503
|
+
|
|
504
|
+
system(%(bash -lc "source #{rvm_script} && rvm --version")) ||
|
|
505
|
+
abort("RVM installed but not functional")
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def ensure_ruby_installed!(version)
|
|
509
|
+
return if system("rvm list strings | grep ruby-#{version} > /dev/null")
|
|
510
|
+
|
|
511
|
+
puts "Installing Ruby #{version}..."
|
|
512
|
+
system("rvm install #{version}") || abort("Failed to install Ruby #{version}")
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def detect_ruby_version(path)
|
|
516
|
+
rv = File.join(path, ".ruby-version")
|
|
517
|
+
return File.read(rv).strip if File.exist?(rv)
|
|
518
|
+
|
|
519
|
+
gemfile = File.join(path, "Gemfile")
|
|
520
|
+
if File.exist?(gemfile)
|
|
521
|
+
ruby_line = File.read(gemfile)[/^ruby ['"](.+?)['"]/, 1]
|
|
522
|
+
return ruby_line if ruby_line
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
nil
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def rvm_available?
|
|
529
|
+
system("bash -lc 'command -v rvm > /dev/null'")
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def rbenv_available?
|
|
533
|
+
system("command -v rbenv > /dev/null")
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def ensure_rvm_ruby!(version)
|
|
537
|
+
system("bash -lc 'rvm list strings | grep -q #{version} || rvm install #{version}'")
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def ensure_rbenv_ruby!(version)
|
|
541
|
+
system("rbenv versions | grep -q #{version} || rbenv install #{version}")
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
end
|
|
545
|
+
end
|
data/lib/stable/paths.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Stable
|
|
2
|
+
module Paths
|
|
3
|
+
def self.root
|
|
4
|
+
File.expand_path("~/StableCaddy")
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def self.caddy_dir
|
|
8
|
+
root
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.caddyfile
|
|
12
|
+
File.join(caddy_dir, "Caddyfile")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.certs_dir
|
|
16
|
+
File.join(root, "certs")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.apps_file
|
|
20
|
+
File.join(root, "apps.yml")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
module Stable
|
|
5
|
+
class Registry
|
|
6
|
+
def self.file_path
|
|
7
|
+
File.join(Stable.root, "apps.yml")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.save(apps)
|
|
11
|
+
FileUtils.mkdir_p(Stable.root)
|
|
12
|
+
File.write(file_path, apps.to_yaml)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.apps
|
|
16
|
+
File.exist?(file_path) ? YAML.load_file(file_path) : []
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/stable.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Stable
|
|
2
|
+
def self.root
|
|
3
|
+
File.expand_path("~/.stable")
|
|
4
|
+
end
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
require "fileutils"
|
|
8
|
+
require_relative "stable/paths"
|
|
9
|
+
require_relative "stable/cli"
|
|
10
|
+
require_relative "stable/registry"
|
|
11
|
+
require_relative "stable/scanner"
|
|
12
|
+
require_relative "stable/bootstrap"
|
|
13
|
+
|
|
14
|
+
|
metadata
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: stable-cli-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Danny Simfukwe
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: thor
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: fileutils
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
description: "# Stable CLI (macOS)\n\nStable is a CLI tool to manage local Rails applications
|
|
41
|
+
with automatic Caddy setup on macOS, local trusted HTTPS certificates, and easy
|
|
42
|
+
start/stop functionality.\n\n## Features\n\n- Add and remove Rails apps.\n- Automatically
|
|
43
|
+
generate and manage local HTTPS certificates using `mkcert`.\n- Automatically update
|
|
44
|
+
`/etc/hosts` for `.test` domains.\n- Start Rails apps with integrated Caddy reverse
|
|
45
|
+
proxy.\n- Reload Caddy after adding/removing apps.\n- List all registered apps.\n\n##
|
|
46
|
+
Installation\n\n### From source\n\n```bash\n# Clone the repository\ngit clone git@github.com:dannysimfukwe/stable-rails.git\ncd
|
|
47
|
+
stable-rails\n\n# Install dependencies\nbundle install\n```\n\n### As a gem\n\n```bash\ngem
|
|
48
|
+
install stable\n```\n\n## Setup\n\nInitialize Caddy home and required directories:\n\n```bash\nstable
|
|
49
|
+
setup\n```\n\nThis will create: \n- `~/StableCaddy/` for Caddy configuration. \n-
|
|
50
|
+
`~/StableCaddy/certs` for generated certificates. \n- `~/StableCaddy/Caddyfile`
|
|
51
|
+
for Caddy configuration. \n\n## CLI Commands\n\n### List apps\n\n```bash\nstable
|
|
52
|
+
list\n```\n\nLists all registered apps and their domains.\n\n### Add a Rails app\n\n```bash\nstable
|
|
53
|
+
add /path/to/rails_app\n```\n\nThis will: \n- Register the app. \n- Add a `/etc/hosts`
|
|
54
|
+
entry. \n- Generate local trusted HTTPS certificates. \n- Add a Caddy reverse
|
|
55
|
+
proxy block. \n- Reload Caddy.\n\n### Remove a Rails app\n\n```bash\nstable remove
|
|
56
|
+
app_name\n```\n\nThis will: \n- Remove the app from registry. \n- Remove `/etc/hosts`
|
|
57
|
+
entry. \n- Remove the Caddy reverse proxy block. \n- Reload Caddy.\n\n### Start
|
|
58
|
+
an app\n\n```bash\nrvmsudo stable start app_name\n```\n\nStarts the Rails server
|
|
59
|
+
on the assigned port and ensures Caddy is running with the proper reverse proxy.
|
|
60
|
+
Rails logs can be viewed in your terminal.\n\n### Stop an app\n\n```bash\nstable
|
|
61
|
+
stop app_name\n```\n\nStops the Rails server running on the assigned port.\n\n###
|
|
62
|
+
Secure an app manually\n\n```bash\nrvmsudo stable secure app_name.test\n```\n\nGenerates
|
|
63
|
+
or updates trusted local HTTPS certificates and reloads Caddy.\n\n### Reload Caddy\n\n```bash\nstable
|
|
64
|
+
caddy reload\n```\n\nReloads Caddy configuration after changes.\n\n### Health check\n\n```bash\nstable
|
|
65
|
+
doctor\n```\n\nChecks the environment, RVM/Ruby, Caddy, mkcert, and app readiness.\n\n###
|
|
66
|
+
Upgrade Ruby for an app\n\n```bash\nstable upgrade-ruby myapp 3.4.4\n```\n\nUpgrades
|
|
67
|
+
the Ruby version for a specific app, updating `.ruby-version` and ensuring gemset
|
|
68
|
+
compatibility.\n\n### Create a new Rails app\n\n```bash\nstable new myapp [--ruby
|
|
69
|
+
3.4.4] [--rails 7.0.7.1] [--skip-ssl]\n```\n\nCreates a new Rails app, generates
|
|
70
|
+
`.ruby-version`, installs Rails, adds the app to Stable, and optionally secures
|
|
71
|
+
it with HTTPS.\n\n## Paths\n\n- Caddy home: `~/StableCaddy` \n- Caddyfile: `~/StableCaddy/Caddyfile`
|
|
72
|
+
\ \n- Certificates: `~/StableCaddy/certs` \n- Registered apps: `~/StableCaddy/apps.yml`
|
|
73
|
+
\ \n\n## Dependencies\n\n- Homebrew \n- Caddy \n- mkcert \n- RVM (or rbenv fallback)\n\n`ensure_dependencies!`
|
|
74
|
+
will install missing dependencies automatically.\n\n## Known Issues\n\n- Sometimes
|
|
75
|
+
you may see: \n```\nTCPSocket#initialize: Connection refused - connect(2) for \"127.0.0.1\"
|
|
76
|
+
port 300.. (Errno::ECONNREFUSED)\n```\nThis usually disappears after a few seconds
|
|
77
|
+
when Caddy reloads. If it persists, run:\n\n```bash\nrvmsudo stable secure myapp.test\n```\n\n-
|
|
78
|
+
Some commands may need to be run consecutively for proper setup: \n```bash\nstable
|
|
79
|
+
setup\nrvmsudo stable add myapp\nrvmsudo stable secure myapp.test\nstable start
|
|
80
|
+
myapp\n```\n\n- PATH warnings from RVM may appear on the first run. Make sure your
|
|
81
|
+
shell is properly configured for RVM.\n\n## Notes\n\n- Make sure to run `stable
|
|
82
|
+
setup` initially. \n- Requires `sudo` to modify `/etc/hosts`. \n- Rails apps are
|
|
83
|
+
started on ports assigned by Stable (default 3000+). \n- Domains are automatically
|
|
84
|
+
suffixed with `.test`. \n\n## License\n\nMIT License\n"
|
|
85
|
+
email:
|
|
86
|
+
- dannysimfukwe@gmail.com
|
|
87
|
+
executables:
|
|
88
|
+
- stable
|
|
89
|
+
extensions: []
|
|
90
|
+
extra_rdoc_files: []
|
|
91
|
+
files:
|
|
92
|
+
- README.md
|
|
93
|
+
- bin/stable
|
|
94
|
+
- lib/stable.rb
|
|
95
|
+
- lib/stable/bootstrap.rb
|
|
96
|
+
- lib/stable/cli.rb
|
|
97
|
+
- lib/stable/paths.rb
|
|
98
|
+
- lib/stable/registry.rb
|
|
99
|
+
- lib/stable/scanner.rb
|
|
100
|
+
homepage: https://github.com/dannysimfukwe/stable-rails
|
|
101
|
+
licenses:
|
|
102
|
+
- MIT
|
|
103
|
+
metadata: {}
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '0'
|
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
requirements: []
|
|
118
|
+
rubygems_version: 3.6.7
|
|
119
|
+
specification_version: 4
|
|
120
|
+
summary: CLI tool to manage local Rails apps with automatic Caddy and HTTPS setup
|
|
121
|
+
test_files: []
|