rails-dev 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/CHANGELOG.md +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +134 -0
- data/docs/reference.md +220 -0
- data/exe/rails-dev +6 -0
- data/lib/rails_dev/child.rb +111 -0
- data/lib/rails_dev/cli/base.rb +41 -0
- data/lib/rails_dev/cli/main.rb +69 -0
- data/lib/rails_dev/cli/services.rb +28 -0
- data/lib/rails_dev/cli/shell.rb +25 -0
- data/lib/rails_dev/cli.rb +44 -0
- data/lib/rails_dev/commands.rb +115 -0
- data/lib/rails_dev/compose.rb +97 -0
- data/lib/rails_dev/configuration.rb +137 -0
- data/lib/rails_dev/cookies.rb +22 -0
- data/lib/rails_dev/endpoint.rb +105 -0
- data/lib/rails_dev/foreman.rb +45 -0
- data/lib/rails_dev/hooks.rb +43 -0
- data/lib/rails_dev/instance.rb +145 -0
- data/lib/rails_dev/output.rb +31 -0
- data/lib/rails_dev/portless.mjs +28 -0
- data/lib/rails_dev/portless.rb +110 -0
- data/lib/rails_dev/ports.rb +20 -0
- data/lib/rails_dev/process_table.rb +32 -0
- data/lib/rails_dev/rails_vite.rb +14 -0
- data/lib/rails_dev/railtie.rb +21 -0
- data/lib/rails_dev/service_ports.rb +88 -0
- data/lib/rails_dev/services.rb +37 -0
- data/lib/rails_dev/session.rb +215 -0
- data/lib/rails_dev/state.rb +60 -0
- data/lib/rails_dev/tailscale.rb +59 -0
- data/lib/rails_dev/version.rb +5 -0
- data/lib/rails_dev/vite.mjs +31 -0
- data/lib/rails_dev.rb +55 -0
- metadata +141 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d79ed132a36f779c8025cd306d4e75f80360de8cfb65409295b8da11bc2badcd
|
|
4
|
+
data.tar.gz: a9db8f42cb70536b5c49adfc58d65bc0736a9483e23473abc6e571f8e98812a0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 323b6bae902098dc3536f66eb643d7b7fadcaeca488a26862a65ad6f3bd4ca469b45e199f137c3025cfb975081a182011b678fd0b720a2610f98dfe482083484
|
|
7
|
+
data.tar.gz: 92deea7d90f997e1d7b6af91d2614c68a133a760fdd49da0f058a65cf100be20c1959eff44cb230e166d61df031dbd4e8918c4e3cc67520edbb06c389aee45cc
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-09-24
|
|
4
|
+
|
|
5
|
+
- Run app Procfiles through Foreman on available ports, including multiple instances from the same checkout.
|
|
6
|
+
- Provide local HTTPS URLs and optional Tailscale sharing.
|
|
7
|
+
- Start and reuse Docker Compose services on available loopback ports with explicit connection mappings.
|
|
8
|
+
- Support global and app configuration, lifecycle hooks, optional processes, and configurable readiness checks.
|
|
9
|
+
- Configure Rails hostnames, URLs, cookies, and PID files, with an optional helper for Rails Vite apps.
|
|
10
|
+
- List instances across projects and recover interrupted runs while preserving shared services.
|
|
11
|
+
- Support Ruby 3.4 and newer on macOS and Linux, with ERB or Vite apps.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ali Hamdi Ali Fadel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# rails-dev
|
|
2
|
+
|
|
3
|
+
Run Rails on an available port, get a local HTTPS URL, and share it through Tailscale when needed. Start several instances from the same checkout; each gets its own URL.
|
|
4
|
+
|
|
5
|
+
Works on macOS and Linux with ERB or Vite apps. Foreman runs your Procfile. Your app chooses its tools and preparation commands; rails-dev handles ports, HTTPS, and optional Docker Compose services.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
| Requirement | When you need it |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Ruby 3.4+ | Always; Foreman is included as a dependency |
|
|
12
|
+
| Node.js 24+ and npm | Local HTTPS |
|
|
13
|
+
| Docker Compose 2.24.4+ | Managed Docker services |
|
|
14
|
+
| Connected Tailscale client | Sharing over your tailnet |
|
|
15
|
+
|
|
16
|
+
Install using the same Ruby as your app:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
gem install rails-dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For a bundled installation, use `bundle add` below and prefix rails-dev commands with `bundle exec`.
|
|
23
|
+
|
|
24
|
+
## Set up your app
|
|
25
|
+
|
|
26
|
+
Use your app's usual shell environment and install its dependencies first. Mise is optional.
|
|
27
|
+
|
|
28
|
+
**1. Add the Rails helper to your app:**
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
bundle add rails-dev --group development,test --require rails_dev/railtie
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The helper configures hostnames, route and mailer URLs, and cookies for each instance. Apps using `rails_vite` also need the [Vite setup](docs/reference.md#vite).
|
|
35
|
+
|
|
36
|
+
**2. Put your server command in `Procfile.dev`:**
|
|
37
|
+
|
|
38
|
+
```procfile
|
|
39
|
+
web: bin/rails server -b 127.0.0.1
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Keep your existing workers and asset watchers there too. Let commands use the supplied ports; fixed ports in commands or `.env` can override them.
|
|
43
|
+
|
|
44
|
+
Configure [Docker services](#add-docker-services) or [preparation hooks](docs/reference.md#hooks) before starting if your app needs them.
|
|
45
|
+
|
|
46
|
+
**3. Set up local HTTPS once per machine, then start from your app directory:**
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
rails-dev setup
|
|
50
|
+
rails-dev start
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`setup` installs the Portless proxy and certificate trust. `start` picks a free port starting at 3000 and prints a URL such as `https://my-app-3001.localhost`.
|
|
54
|
+
|
|
55
|
+
Startup waits for `GET /up` to return HTTP 200. [Change the readiness check](docs/reference.md#readiness) if your app uses another health endpoint.
|
|
56
|
+
|
|
57
|
+
Run `start` in another terminal for a second instance. **Ctrl+C stops that instance**; shared Docker services stay running.
|
|
58
|
+
|
|
59
|
+
## Everyday commands
|
|
60
|
+
|
|
61
|
+
| Task | Command |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| Start an instance | `rails-dev start` |
|
|
64
|
+
| List your instances across projects | `rails-dev list` |
|
|
65
|
+
| List one checkout's instances | `rails-dev list --root .` |
|
|
66
|
+
| Stop an instance using its listed ID | `rails-dev stop INSTANCE` |
|
|
67
|
+
| Check local HTTPS | `rails-dev doctor` |
|
|
68
|
+
| See command options | `rails-dev --help` |
|
|
69
|
+
|
|
70
|
+
If a terminal or launcher exits unexpectedly, use `list` and `stop` to recover the instance. [Status and recovery details](docs/reference.md#instances-and-recovery).
|
|
71
|
+
|
|
72
|
+
## Add Docker services
|
|
73
|
+
|
|
74
|
+
Create `config/rails_dev.rb` in your app. For PostgreSQL:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
RailsDev.configure do |config|
|
|
78
|
+
config.compose_file = "dev-docker-compose.yml"
|
|
79
|
+
config.connect "postgres", port: 5432,
|
|
80
|
+
env: { "PGHOST" => "%{host}", "PGPORT" => "%{port}" }
|
|
81
|
+
|
|
82
|
+
config.before_start { |run| run.run("bin/rails", "db:prepare") }
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use your Compose filename and service name. Rails-dev starts enabled services and publishes their declared ports on available loopback ports. It supplies the connection variables you map; your app provides database names and credentials.
|
|
87
|
+
|
|
88
|
+
If your app uses `DATABASE_URL`, map it instead of leaving a fixed URL that overrides `PGHOST` and `PGPORT`. See [Compose configuration](docs/reference.md#docker-compose) for Redis, ClickHouse, profiles, and exclusions.
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
rails-dev services exec -- bin/rails console
|
|
92
|
+
RAILS_ENV=test rails-dev services exec -- bin/rails test
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
These commands start or reuse services and pass their connections to the host command. Rails does not need to be running.
|
|
96
|
+
|
|
97
|
+
## Share through Tailscale
|
|
98
|
+
|
|
99
|
+
Connect Tailscale and enable HTTPS in your tailnet's DNS settings, then run:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
rails-dev start --tailscale
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Open the printed URL from another device allowed to reach your machine on the tailnet. Each instance uses its own HTTPS port. [Tailscale permissions and setup](docs/reference.md#tailscale).
|
|
106
|
+
|
|
107
|
+
## Customize
|
|
108
|
+
|
|
109
|
+
App settings live in `config/rails_dev.rb` and override global defaults. Keep personal overrides in `config/rails_dev.local.rb` and add that file to your app's ignore list.
|
|
110
|
+
|
|
111
|
+
| What you want to change | Guide |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| App name, ports, Procfile, or defaults | [Configuration](docs/reference.md#configuration) |
|
|
114
|
+
| Environment and process commands | [Processes and environment](docs/reference.md#processes-and-environment) |
|
|
115
|
+
| More HTTP servers | [Additional HTTP servers](docs/reference.md#additional-http-servers) |
|
|
116
|
+
| Preparation and lifecycle hooks | [Hooks](docs/reference.md#hooks) |
|
|
117
|
+
| Optional processes, such as Stripe | [Optional processes](docs/reference.md#optional-processes) |
|
|
118
|
+
| Vite URLs and hot reload | [Vite](docs/reference.md#vite) |
|
|
119
|
+
| Independent restarts and shared assets | [Multiple runs](docs/reference.md#multiple-runs-from-one-checkout) |
|
|
120
|
+
| Plain HTTP or a different HTTPS proxy port | [Local HTTPS](docs/reference.md#local-https) |
|
|
121
|
+
|
|
122
|
+
OAuth relay, public webhook relay, and direct LAN HTTPS are deferred.
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
mise ci
|
|
128
|
+
mise run test:docker
|
|
129
|
+
mise run test:https
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Without Mise, use `bundle exec rake test rubocop build`, `bundle exec rake test_docker`, and `bundle exec rake test_https`.
|
|
133
|
+
|
|
134
|
+
Tests use Minitest. CI covers Ruby 3.4 and 4.0 on Linux and macOS.
|
data/docs/reference.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Configuration reference
|
|
2
|
+
|
|
3
|
+
See the [README](../README.md) for installation and everyday commands. Ruby examples below belong in `config/rails_dev.rb` unless another file is named.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
Settings load in order:
|
|
8
|
+
|
|
9
|
+
1. Gem defaults.
|
|
10
|
+
2. `$XDG_CONFIG_HOME/rails-dev/config.rb`, defaulting to `~/.config/rails-dev/config.rb`.
|
|
11
|
+
3. Your app's `config/rails_dev.rb`.
|
|
12
|
+
4. Your app's optional `config/rails_dev.local.rb`.
|
|
13
|
+
|
|
14
|
+
Later assignments replace earlier values. Arrays and hooks can be extended or cleared. Repeated `http` declarations merge options, including readiness settings.
|
|
15
|
+
|
|
16
|
+
| Setting | Default |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `name` | Derived from the checkout directory name |
|
|
19
|
+
| `procfile` | `"Procfile.dev"` |
|
|
20
|
+
| `web` | `"web"`, the primary HTTP process |
|
|
21
|
+
| `https` | `true` |
|
|
22
|
+
| `startup_timeout` | 90 seconds |
|
|
23
|
+
| `shutdown_timeout` | 5 seconds |
|
|
24
|
+
| `compose_file` | Unset; Docker management is off |
|
|
25
|
+
|
|
26
|
+
Use `--root PATH` to select the working directory, even when the Procfile lives in a subdirectory.
|
|
27
|
+
|
|
28
|
+
## Processes and environment
|
|
29
|
+
|
|
30
|
+
Foreman runs your original Procfile and handles sibling shutdown when a process exits. Its normal `.env` behavior applies. Values loaded by Foreman are unavailable to earlier preparation hooks; the app must supply those separately.
|
|
31
|
+
|
|
32
|
+
| Variable | Meaning |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `PORT` | Selected port in the primary web process; other processes follow Foreman's usual numbering |
|
|
35
|
+
| `RAILS_PORT` | Primary web port, available to every process |
|
|
36
|
+
| `RAILS_DEV_URL` | Primary public URL |
|
|
37
|
+
| `RAILS_DEV_RUNTIME` | This instance's runtime directory |
|
|
38
|
+
|
|
39
|
+
Procfile commands have no terminal input. Use `services exec` for interactive commands. Rails-dev inherits your shell environment and removes the launcher's Bundler settings from child processes.
|
|
40
|
+
|
|
41
|
+
## Docker Compose
|
|
42
|
+
|
|
43
|
+
Extend the [PostgreSQL example](../README.md#add-docker-services) with the connections and service selections your app needs:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
RailsDev.configure do |config|
|
|
47
|
+
config.connect "redis", port: 6379,
|
|
48
|
+
env: { "REDIS_URL" => "redis://%{host}:%{port}/0" }
|
|
49
|
+
config.connect "clickhouse", port: 8123,
|
|
50
|
+
env: { "CLICKHOUSE_URL" => "http://%{host}:%{port}" }
|
|
51
|
+
config.profiles = ["debug"]
|
|
52
|
+
config.exclude_services = ["mail"]
|
|
53
|
+
config.publish_ports["internal"] = [6379]
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Templates accept `%{host}` and `%{port}`. Credentials and database names remain app settings. An inherited `DATABASE_URL` with a PGPORT-only mapping is rejected; map the URL or remove it from the launch environment.
|
|
58
|
+
|
|
59
|
+
- All Compose-enabled services start, respecting dependencies and profiles. Any service type can use explicit connection mappings.
|
|
60
|
+
- Declared publications and requested ports receive available loopback ports. Existing containers are reused.
|
|
61
|
+
- Healthchecks determine readiness where present; otherwise running status is enough.
|
|
62
|
+
- Required dependencies cannot be excluded. Host networking, scaled services, and conflicting or non-loopback publications are unsupported.
|
|
63
|
+
|
|
64
|
+
`rails-dev services start` starts shared services. `rails-dev services stop` stops them for **every instance**, keeping containers and volumes. Both require Compose configuration.
|
|
65
|
+
|
|
66
|
+
`services exec` preserves arguments, terminal input, and exit status. It skips web hooks and preserves `RAILS_ENV`. Without Compose configured, it uses the existing environment.
|
|
67
|
+
|
|
68
|
+
## Additional HTTP servers
|
|
69
|
+
|
|
70
|
+
Match the endpoint name to its Procfile process and declare the port variable that command consumes:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
RailsDev.configure do |config|
|
|
74
|
+
config.http :js, port: 5173, env: "VITE_PORT",
|
|
75
|
+
url_env: "RAILS_DEV_VITE_URL", readiness: "/@vite/client"
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Add to `Procfile.dev`:
|
|
80
|
+
|
|
81
|
+
```procfile
|
|
82
|
+
js: pnpm exec vite --host 127.0.0.1 --port ${VITE_PORT:-5173} --strictPort
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Here, `js` is the process name. Vite's `--strictPort` makes a taken port fail; rails-dev retries confirmed bind conflicts without repeating preparation hooks.
|
|
86
|
+
|
|
87
|
+
Each endpoint gets a URL such as `https://my-app-3001-js.localhost`. `url_env` defaults to `RAILS_DEV_<PROCESS>_URL`, using uppercase and replacing hyphens with underscores. Processes that do not serve HTTP need no endpoint declaration.
|
|
88
|
+
|
|
89
|
+
## Hooks
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
RailsDev.configure do |config|
|
|
93
|
+
config.around_run do |run, continue|
|
|
94
|
+
continue.call
|
|
95
|
+
ensure
|
|
96
|
+
run.say("Run finished")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Hook | When it runs |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| `before_start` | After services and connection mapping, before Foreman |
|
|
104
|
+
| `after_ready` | After enabled readiness checks pass |
|
|
105
|
+
| `around_run` | Around preparation, startup, and the app run; call its continuation once |
|
|
106
|
+
| `after_stop` | After processes stop and around hooks unwind, including on errors and interrupts |
|
|
107
|
+
|
|
108
|
+
Hooks run once per invocation. Before/ready hooks run in registration order; stop hooks run in reverse order and all are attempted. Cleanup errors do not replace an earlier failure. Clear inherited hooks with `config.before_start.clear`.
|
|
109
|
+
|
|
110
|
+
The context exposes `root`, `runtime`, `env`, `url(process = primary_web)`, `error`, and `enabled?(name)`. URLs become available after networking is allocated.
|
|
111
|
+
|
|
112
|
+
`run(*argv)` streams output; `capture(*argv)` returns stdout. Both use argument arrays without a shell and run in the app directory with `run.env`. Use `say(message)` for output and `check_for_interrupt` during long custom work.
|
|
113
|
+
|
|
114
|
+
## Optional processes
|
|
115
|
+
|
|
116
|
+
Declare a Procfile process with `config.optional` and select it through `--with`. For Stripe, install its CLI and run `stripe login` first:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
RailsDev.configure do |config|
|
|
120
|
+
config.optional :stripe
|
|
121
|
+
config.before_start do |run|
|
|
122
|
+
next unless run.enabled?(:stripe)
|
|
123
|
+
|
|
124
|
+
run.env["STRIPE_SIGNING_SECRET"] = run.capture("stripe", "listen", "--print-secret").strip
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Add to `Procfile.dev`, using your app's webhook path:
|
|
130
|
+
|
|
131
|
+
```procfile
|
|
132
|
+
stripe: stripe listen --skip-update --forward-to http://127.0.0.1:${RAILS_PORT}/pay/webhooks/stripe
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Start with `rails-dev start --with stripe`. Foreman starts selected processes together, so webhooks can arrive before Rails is ready.
|
|
136
|
+
|
|
137
|
+
- `config.optional :listener, with: :payments` lets a selection enable a differently named process. Several processes can share one selection.
|
|
138
|
+
- For optional hook work alone, add a name to `config.integrations` and check `run.enabled?`. Unknown selections fail before startup.
|
|
139
|
+
|
|
140
|
+
## Readiness
|
|
141
|
+
|
|
142
|
+
The primary endpoint defaults to `GET /up`, expecting HTTP 200; other endpoints need an explicit check. Requests use loopback HTTP with the public hostname in the Host header.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
RailsDev.configure do |config|
|
|
146
|
+
config.readiness = { request_timeout: 1, interval: 0.1 }
|
|
147
|
+
config.http :web, readiness: { path: "/health", success: 200, timeout: 30 }
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
| Option | Meaning |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `path`, `method`, `headers`, `body` | Request settings; method defaults to `GET` |
|
|
154
|
+
| `success` | Status code, array of codes, or callable receiving the response; defaults to `200` |
|
|
155
|
+
| `timeout` | Endpoint timeout, also bounded by `startup_timeout` |
|
|
156
|
+
| `request_timeout` | Timeout for each connection, read, or write; defaults to 1 second |
|
|
157
|
+
| `interval` | Polling interval; defaults to 0.1 seconds |
|
|
158
|
+
|
|
159
|
+
`config.readiness` supplies shared defaults; endpoint settings override them. Use `readiness: false` to disable a check or `readiness: ->(endpoint) { ... }` for a custom check. The endpoint exposes `name`, `port`, and `url`.
|
|
160
|
+
|
|
161
|
+
Custom checks must return promptly and manage their own timeouts. Checks gate `after_ready` and the Ready message, not process launch order or ongoing health. A timeout stops owned processes and removes their routes.
|
|
162
|
+
|
|
163
|
+
## Vite
|
|
164
|
+
|
|
165
|
+
For `rails_vite` and `rails-vite-plugin`, load both helpers in your Gemfile's development/test group:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
gem "rails-dev", require: ["rails_dev/railtie", "rails_dev/rails_vite"]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Declare the [HTTP endpoint](#additional-http-servers) with `url_env: "RAILS_DEV_VITE_URL"`. Add the helper to your existing Vite configuration, keeping your other plugins. Its returned options can be customized:
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
export default defineConfig(async ({ command }) => {
|
|
175
|
+
const runtime = command === 'serve' ? process.env.RAILS_DEV_RUNTIME : undefined
|
|
176
|
+
const development = runtime ? (await import(`${runtime}/vite.mjs`)).default() : undefined
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
plugins: [development?.plugin, rails(development?.rails)],
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Multiple runs from one checkout
|
|
185
|
+
|
|
186
|
+
Ports, URLs, Rails PID files, cookies, Vite metadata, and Vite caches are separate per run. Compiled JavaScript and CSS remain shared.
|
|
187
|
+
|
|
188
|
+
For independent restarts, make Puma's shared restart watcher conditional in `config/puma.rb`:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
plugin :tmp_restart unless ENV["RAILS_DEV_RUNTIME"]
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Local HTTPS
|
|
195
|
+
|
|
196
|
+
Use `rails-dev setup --port 8443` for another proxy port, or `config.https = false` for loopback HTTP. `.localhost` URLs work only on the developer's own machine.
|
|
197
|
+
|
|
198
|
+
## Tailscale
|
|
199
|
+
|
|
200
|
+
Follow the [Tailscale setup](../README.md#share-through-tailscale). On Linux, an administrator may need to grant operator access:
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
sudo tailscale set --operator="$USER"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Tailscale mode needs no local Portless setup. Each endpoint gets an HTTPS port on the machine's tailnet hostname. Existing Serve routes and tailnet permissions are preserved; Funnel is not enabled.
|
|
207
|
+
|
|
208
|
+
## Instances and recovery
|
|
209
|
+
|
|
210
|
+
| Status | Meaning |
|
|
211
|
+
| --- | --- |
|
|
212
|
+
| `starting` | Preparation or startup checks are running |
|
|
213
|
+
| `ready` | Startup and ready hooks completed |
|
|
214
|
+
| `orphaned` | The launcher exited, but verified processes remain |
|
|
215
|
+
| `stale` | No processes remain in the recorded groups |
|
|
216
|
+
| `interrupted` | Process ownership cannot be verified |
|
|
217
|
+
|
|
218
|
+
Status and saved URLs are not ongoing health checks. Use `rails-dev stop INSTANCE` for recovery; cleanup is not automatic after a forced launcher exit.
|
|
219
|
+
|
|
220
|
+
Unverified ownership keeps the record for inspection. Detached daemons and unrecorded processes cannot be recovered this way. Shared Docker services and the HTTPS proxy stay running.
|
data/exe/rails-dev
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsDev
|
|
4
|
+
class Child
|
|
5
|
+
attr_reader :pid, :name, :stdout, :stderr, :status
|
|
6
|
+
|
|
7
|
+
def initialize(name, argv, env:, root:, output: nil, ready_message: nil, keep_stdin_open: false)
|
|
8
|
+
@name = name
|
|
9
|
+
@output = output
|
|
10
|
+
@stdout = +""
|
|
11
|
+
@stderr = +""
|
|
12
|
+
@readers = {}
|
|
13
|
+
@ready_message = ready_message
|
|
14
|
+
@ready = ready_message.nil?
|
|
15
|
+
spawn(argv, env, root, keep_stdin_open)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def poll
|
|
19
|
+
@readers.each { |reader, buffer| drain(reader, buffer) unless reader.closed? }
|
|
20
|
+
@status ||= Process.waitpid2(pid, Process::WNOHANG)&.last
|
|
21
|
+
@status
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def running?
|
|
25
|
+
poll.nil?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ready?
|
|
29
|
+
@ready
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def failure_message
|
|
33
|
+
detail = stderr.strip.empty? ? stdout.strip : stderr.strip
|
|
34
|
+
["#{name} failed (#{exit_reason}).", detail].reject(&:empty?).join("\n")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def exit_reason
|
|
38
|
+
return "exit status #{status.exitstatus}" unless status.signaled?
|
|
39
|
+
|
|
40
|
+
"signal #{Signal.signame(status.termsig) || status.termsig}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def startup_conflict?
|
|
44
|
+
output.match?(/EADDRINUSE|Address already in use|Port \d+ is already in use/i) ||
|
|
45
|
+
(name.start_with?("tailscale-") &&
|
|
46
|
+
output.match?(/listener already exists for port|Another client is changing the serve config/))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reap
|
|
50
|
+
@status ||= Process.waitpid2(pid).last
|
|
51
|
+
poll
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def group_alive?
|
|
55
|
+
!!signal(0)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def signal(value)
|
|
59
|
+
Process.kill(value, -pid)
|
|
60
|
+
rescue Errno::ESRCH
|
|
61
|
+
nil
|
|
62
|
+
rescue Errno::EPERM
|
|
63
|
+
raise unless ProcessTable.new.members([pid]).empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def output
|
|
67
|
+
"#{stdout}\n#{stderr}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def close
|
|
71
|
+
@readers.each_key { |reader| reader.close unless reader.closed? }
|
|
72
|
+
@input&.close
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def spawn(argv, env, root, keep_stdin_open)
|
|
78
|
+
in_read, @input = IO.pipe if keep_stdin_open
|
|
79
|
+
out_read, out_write = IO.pipe
|
|
80
|
+
err_read, err_write = IO.pipe
|
|
81
|
+
@readers = { out_read => @stdout, err_read => @stderr }
|
|
82
|
+
command = [argv.first, argv.first]
|
|
83
|
+
@pid = Process.spawn(env, command, *argv.drop(1), chdir: root, unsetenv_others: true, pgroup: true,
|
|
84
|
+
in: in_read || File::NULL, out: out_write, err: err_write)
|
|
85
|
+
rescue Errno::ENOENT
|
|
86
|
+
close
|
|
87
|
+
raise Error, "Command not found: #{argv.first}. Make it available on PATH before starting rails-dev."
|
|
88
|
+
ensure
|
|
89
|
+
in_read&.close
|
|
90
|
+
out_write&.close
|
|
91
|
+
err_write&.close
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def drain(reader, buffer)
|
|
95
|
+
loop do
|
|
96
|
+
chunk = reader.read_nonblock(8192, exception: false)
|
|
97
|
+
break if chunk == :wait_readable
|
|
98
|
+
return reader.close if chunk.nil?
|
|
99
|
+
|
|
100
|
+
record_output(buffer, chunk)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def record_output(buffer, chunk)
|
|
105
|
+
buffer << chunk
|
|
106
|
+
@ready ||= buffer.include?(@ready_message) if @ready_message
|
|
107
|
+
@output&.write("[#{name}] #{chunk}")
|
|
108
|
+
buffer.replace(buffer.byteslice(-32_768, 32_768)) if @output && buffer.bytesize > 32_768
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsDev
|
|
4
|
+
module CLI
|
|
5
|
+
class Base < Thor
|
|
6
|
+
def self.exit_on_failure?
|
|
7
|
+
true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
remove_command :tree
|
|
11
|
+
check_unknown_options!
|
|
12
|
+
|
|
13
|
+
class_option :root, type: :string, banner: "PATH",
|
|
14
|
+
desc: "App directory (default: current directory; list/stop: all projects)"
|
|
15
|
+
class_option :help, type: :boolean, aliases: "-h", desc: "Show help"
|
|
16
|
+
class_option :version, type: :boolean, aliases: "-v", desc: "Print version"
|
|
17
|
+
|
|
18
|
+
no_commands do
|
|
19
|
+
def invoke_command(command, *arguments)
|
|
20
|
+
if options[:version]
|
|
21
|
+
say VERSION
|
|
22
|
+
elsif options[:help]
|
|
23
|
+
help(command.name == "help" ? nil : command.name)
|
|
24
|
+
else
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def root
|
|
33
|
+
@root ||= File.realpath(options[:root] || Dir.pwd)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def configuration
|
|
37
|
+
Configuration.load(root)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsDev
|
|
4
|
+
module CLI
|
|
5
|
+
class Main < Base
|
|
6
|
+
desc "setup", "Install the pinned Portless version, trust its CA, and start the shared HTTPS proxy"
|
|
7
|
+
option :port, type: :numeric, default: 443, desc: "HTTPS proxy port"
|
|
8
|
+
def setup
|
|
9
|
+
Portless.new(Commands.new(root, output: shell.output)).setup(port: options[:port])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
desc "doctor", "Check the shared proxy, certificates, and hostname resolution"
|
|
13
|
+
def doctor
|
|
14
|
+
Portless.new(Commands.new(root, output: shell.output)).doctor
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc "start", "Run the app's Procfile with available ports and HTTPS"
|
|
18
|
+
option :with, type: :array, banner: "NAME [NAME...]", desc: "Enable optional integrations declared by the app"
|
|
19
|
+
option :tailscale, type: :boolean, default: false, desc: "Share this run over HTTPS on your tailnet"
|
|
20
|
+
def start
|
|
21
|
+
config = configuration
|
|
22
|
+
config.enable_integrations(options[:with])
|
|
23
|
+
Session.new(root, config, tailscale: options[:tailscale], output: shell.output).start
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc "list", "List your active and interrupted instances across projects"
|
|
27
|
+
def list
|
|
28
|
+
selected_root = root if options[:root]
|
|
29
|
+
runs = State.new.runs
|
|
30
|
+
runs.select! { |run| run.fetch("root") == selected_root } if selected_root
|
|
31
|
+
return say("No instances.") if runs.empty?
|
|
32
|
+
|
|
33
|
+
rows = runs.flat_map { |run| instance_rows(run) }
|
|
34
|
+
print_table([%w[INSTANCE APP PID STATUS CHECKOUT ENDPOINT URL], *rows], truncate: false)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "stop INSTANCE", "Stop one instance and recover its interrupted processes"
|
|
38
|
+
def stop(instance)
|
|
39
|
+
State.new.stop(instance, root: options[:root] ? root : nil)
|
|
40
|
+
say "Stopped #{instance}."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
desc "services COMMAND", "Manage Compose services and run host commands"
|
|
44
|
+
subcommand "services", Services
|
|
45
|
+
stop_on_unknown_option! :services
|
|
46
|
+
|
|
47
|
+
no_commands do
|
|
48
|
+
remove_method :services
|
|
49
|
+
|
|
50
|
+
def services(*arguments)
|
|
51
|
+
raise Thor::InvocationError, "Specify a command. See rails-dev services --help." if arguments.empty?
|
|
52
|
+
|
|
53
|
+
Services.start(arguments, debug: true, shell: shell, class_options: options)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def instance_rows(run)
|
|
60
|
+
urls = run.fetch("urls")
|
|
61
|
+
urls = { "" => "" } if urls.empty?
|
|
62
|
+
urls.each_with_index.map do |(name, url), index|
|
|
63
|
+
fields = index.zero? ? run.values_at("id", "name", "pid", "status", "root") : Array.new(5, "")
|
|
64
|
+
fields + [name, url]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsDev
|
|
4
|
+
module CLI
|
|
5
|
+
class Services < Base
|
|
6
|
+
desc "start", "Start or reuse Compose services"
|
|
7
|
+
def start
|
|
8
|
+
run("start")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc "stop", "Stop Compose services, keeping containers and volumes"
|
|
12
|
+
def stop
|
|
13
|
+
run("stop")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "exec -- COMMAND [ARGS]", "Run a host command with service connection settings"
|
|
17
|
+
def exec(command, *arguments)
|
|
18
|
+
run("exec", [command, *arguments])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def run(action, arguments = [])
|
|
24
|
+
RailsDev::Services.new(root, configuration, output: shell.output).run(action, arguments)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsDev
|
|
4
|
+
module CLI
|
|
5
|
+
class Shell < Thor::Shell::Basic
|
|
6
|
+
attr_reader :output
|
|
7
|
+
|
|
8
|
+
def initialize(output, error)
|
|
9
|
+
super()
|
|
10
|
+
@output = output
|
|
11
|
+
@error = error
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def stdout
|
|
17
|
+
@output
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def stderr
|
|
21
|
+
@error
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|