yamine 0.3.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 03b07451fa5f83423efb747feb496c8617bef2fde5b4f4bf0d595d759efd1083
4
+ data.tar.gz: 64fd53920c5f4740402982dc9f56f07a835c83fb5c2a7eb741c636573b69cfad
5
+ SHA512:
6
+ metadata.gz: 73b3fbdd108a91942ba53c29a41cd73825d3f3ea11a24ebc60c42786a792dc74553fe67938b28b2d4787011b021542bf17a633713d1178caa231a359bdd344a9
7
+ data.tar.gz: 315127dad685afebc614c3c76d8e8ecda53413aef997cea0e82730f8a169cbb066a4bcedb25c78cf502631062751b9911f8986842beac14440c4a9f25d4c5e9c
data/CHANGELOG.md ADDED
@@ -0,0 +1,243 @@
1
+ # Changelog
2
+
3
+ ## [0.3.0] — Unreleased
4
+
5
+ Renamed from `ask-local` to **`yamine`** (يمين, "right hand" — the local
6
+ half of the Kamal pair; see README for the full story). Fresh gem, new
7
+ history: starts at 0.3.0 to honor the 0.2.x ask-local lineage without
8
+ claiming continuity. `Ask::Local` → `Yamine`, `ask-local`/`askl` binaries
9
+ → `yamine`, `ASK_LOCAL_*` env → `YAMINE_*`, state dir `~/.ask-local` →
10
+ `~/.yamine`, skill `ask-local` → `yamine`, health headers
11
+ `x-ask-local*` → `x-yamine*`. No migration path — no existing users.
12
+
13
+ ## [0.2.1] — 2026-09-08
14
+
15
+ Patch release focused on making `yamine setup` dependable end-to-end:
16
+ it now installs the 443 service and *finishes* (hosts + doctor), works
17
+ passwordless for agents, survives first-run edge cases, and shows what it
18
+ is doing while it works.
19
+
20
+ ### Fixed
21
+
22
+ - **`setup` no longer stops after installing the service.** The elevated
23
+ install used to `exit` mid-flow, so `/etc/hosts` was never synced and
24
+ `doctor` never ran despite the "setup complete" promise. Install and
25
+ uninstall now return booleans and the CLI owns the exit codes, so
26
+ `setup` always reaches the hosts and doctor steps. (`service uninstall`
27
+ also crashed outright — it called its handler without the required
28
+ context argument.)
29
+ - **A stale user-owned plist is healed on install.** `File.write` keeps
30
+ an existing file's owner, so a leftover plist from an older version
31
+ stayed user-owned and launchd refused it forever with "Bootstrap
32
+ failed: 5". Install now chowns the plist to root explicitly (launchd
33
+ and systemd).
34
+ - **The TLS proxy survives plaintext probes.** The health probe sends
35
+ plain HTTP before TLS; an unhandled `SSL_accept` error used to kill
36
+ the acceptor thread, and with both acceptors gone the daemon exited
37
+ before the readiness probe ever succeeded. A plaintext connection is
38
+ now just one dropped connection.
39
+ - `clean` referenced the CA common name unqualified (NameError) — fixed.
40
+ - `setup --no-service` printed a duplicate "2/3" step — numbered 1/4–4/4.
41
+
42
+ ### Added
43
+
44
+ - `yamine sudoers` — prints the scoped NOPASSWD rules so agents and
45
+ CI can install and run the 443 service without a TTY. Non-interactive
46
+ elevation uses `sudo -n` (never prompts) and points at the grant when
47
+ it is missing; interactive failures just say re-run.
48
+ - The root install syncs `/etc/hosts` under elevation, so Safari works
49
+ the moment setup finishes. `setup`'s hosts step verifies the block is
50
+ already present instead of failing unprivileged on re-runs, and
51
+ `yamine hosts sync` re-runs itself elevated when the direct write
52
+ needs root.
53
+ - The CA is trusted into the System keychain silently while elevated
54
+ (all users, no GUI popup); unprivileged trust keeps the login
55
+ keychain.
56
+ - Progress UX: the root half of the install prints stage lines as it
57
+ works, and the proxy-start wait shows a rotating spinner on a terminal
58
+ (dots elsewhere), resolving to a clean "proxy is up" line.
59
+
60
+ ### Changed
61
+
62
+ - launchctl uses the modern system-domain verbs
63
+ (`bootstrap`/`bootout`/`enable`/`kickstart` — the puma-dev/portless
64
+ pattern); the legacy `load`/`unload` are rejected by current macOS.
65
+ The best-effort pre-install bootout is silenced — its "Boot-out
66
+ failed: 5" noise on a first install looked like a failure.
67
+ - Every privileged execution path (sudo re-exec, launchctl, systemctl,
68
+ `security`) flows through one injectable `Command` seam, so the test
69
+ suite never shells out to real privileged commands.
70
+
71
+ ### Tests
72
+
73
+ - Hermetic and CI-safe suite: no real sudo/security/launchctl in tests;
74
+ exact-argument expectations on the `Command` seam, source-pinned
75
+ regressions for the launchctl verbs, root-owned plist and stage
76
+ progress.
77
+ - New coverage: setup runs the real elevation path to "Setup complete"
78
+ (regression for the mid-flow exit), hosts-step write/verify/skip
79
+ behavior, `Hosts.synced?`, terminal spinner vs. off-tty dots, e2e
80
+ proof that a TLS daemon survives the plaintext probe, and both
81
+ elevation-failure hint paths. 182 unit + 23 e2e, all green.
82
+
83
+
84
+ ## [0.2.0] — 2026-09-07
85
+
86
+ ### Changed
87
+
88
+ - `cli.rb` (757 lines) split into command objects behind a shared
89
+ Context: `cli/boot.rb` (run/boot/supervision), `cli/routes.rb`
90
+ (get/alias/list/prune/stop/restart/log/status/open),
91
+ `cli/system.rb` (proxy/service/hosts/trust/clean/doctor/kamal).
92
+ - Default `rake test` runs the fast unit suite (~6s); `rake test:e2e`
93
+ runs daemon/TLS/live-boot tests; `rake test:all` runs everything.
94
+
95
+ ### Added
96
+
97
+ - Daemon-owned supervision (puma-dev model): managed apps idle-stop
98
+ after 15 minutes (`YAMINE_IDLE_TIMEOUT`), stop when tmp/restart.txt
99
+ changes, and boot transparently on the next request.
100
+ - `yamine stop` (exit 0 stopped / 2 no route / 3 backend already
101
+ gone), `restart`, `log [-f] [n]`, `status` (effective naming context),
102
+ `open [name]` (browser). `list` shows backend liveness per route.
103
+ - Root-owned `service install` (launchd/systemd) binding 80/443 at boot
104
+ with the invoking user's state dir; sudo re-exec when needed.
105
+ - All root write paths chown state back to the invoking user; `doctor`
106
+ reports an unwritable state dir plainly. The privileged auto-start
107
+ re-execs under `sudo` with the correct state dir.
108
+ - Bounded proxy concurrency (`YAMINE_MAX_CONNECTIONS`, 503 past the
109
+ cap), mtime-based route cache (no TTL race for boot-then-curl), IPv4+IPv6
110
+ loopback listeners, dual-stack `ours?` health check.
111
+ - `get` inherits variant/TLD context from the current directory
112
+ (`get backend` in a fix-ui worktree -> fix-ui.backend.localhost);
113
+ `--service/--variant/--tld` overrides.
114
+ - `alias` accepts full hostnames on any TLD and honors YAMINE_TLD.
115
+ - `kamal` snippet resolves the app from the directory; `--app/--domain`
116
+ flags and YAMINE_KAMAL_DOMAIN.
117
+ - `clean` untrusts the CA from the OS trust store.
118
+ - `--proc <name>` picks a specific Procfile process.
119
+ - Ships the `yamine` agent skill (ask/skills/yamine/SKILL.md).
120
+ - `test:e2e` / `test:all` rake tasks; CI matrix (3.2/3.3/3.4/4.0),
121
+ macOS e2e leg, fixture-sweep job.
122
+ - Ownership module, framework fixtures (sinatra-modular,
123
+ foreman-`$PORT`, hanami2 slice layout, jekyll livereload), SKILL.md
124
+ "when NOT to use" section, README non-goals, vite/Shakapacker recipe.
125
+ - Host authorization patterns default TLDs from `YAMINE_TLD`.
126
+ - WebSocket Upgrade end-to-end test (RFC 6455 handshake + frame echo),
127
+ hop-loop 508 rejection, chunked framing, and spinning-loop fix.
128
+
129
+ ### Fixed
130
+
131
+ - Daemon spawn and service install resolved the yamine binary one
132
+ directory too high — `proxy start` failed outright; failures now
133
+ include the proxy log tail.
134
+ - Keep-alive connections now rewrite headers (X-Forwarded-Proto) on
135
+ every request, not just the first — request 2+ previously reached the
136
+ backend unrewritten, breaking ssl detection and OmniAuth callbacks.
137
+ - Ctrl+C / TERM stops the backend process (no orphans); the CLI exits
138
+ when the backend dies, cleaning up routes.
139
+ - SNI certificate minting is arity-agnostic across ruby-openssl versions
140
+ (callback args differ; a raise surfaced as an unrecognized-name alert).
141
+ - Proxy honors HTTP/1.1 framing per request: Content-Length bodies are
142
+ forwarded exactly; chunked bodies and responses close-delimit.
143
+ - Bidirectional streaming terminates promptly on `Connection: close`
144
+ (each pump direction closes its peer on EOF).
145
+ - `alias --remove` now appends the default TLD.
146
+ - Install generator `source_root` pointed at a doubled path; generator
147
+ file checks now resolve against `destination_root`.
148
+ - Port-flag injection no longer double-sets an explicit `$PORT`.
149
+
150
+
151
+ ## [0.1.1] — 2026-09-06
152
+
153
+ Patch release focused on workstation setup, URL correctness, and proxy reliability.
154
+
155
+ ### Added — `yamine setup` & `yamine start`
156
+
157
+ - `yamine setup` — one-shot workstation setup for clean
158
+ `https://<app>.localhost` URLs: trust the local CA, serve port 443
159
+ (root launchd/systemd service when possible, sudo daemon otherwise),
160
+ sync `/etc/hosts`, and verify with `doctor`. Each step reports
161
+ `==>` / `ok` and the first failure aborts with the specific fix.
162
+ - `yamine start` — one-setup-and-go entry point: an idempotent
163
+ workstation-check-then-boot (`yamine setup` if needed, then the
164
+ app). `yamine` bare is an alias for it; `yamine setup` stays for
165
+ explicit re-setup.
166
+ - `askl` — shell-friendly alias binary (`bin/askl`, same entry point as
167
+ `bin/yamine`). Keep `yamine` in logs and docs so `grep` stays
168
+ useful.
169
+
170
+ ### Added — DNS-rebinding & log hygiene
171
+
172
+ - DNS-rebinding boundary: foreign `Host` headers get a bare 404 naming
173
+ nothing; only hosts under our own configured TLDs see the route-listing
174
+ 404. The proxy takes `--tld` (persisted to `proxy.tlds`) so the boundary
175
+ follows custom domains. The `X-Ask-Local: 1` health header marks our
176
+ proxy responses (including 404s) for the `ours?` probe.
177
+ - Log rotation — `proxy.log` and per-app backend logs rotate at 5MB
178
+ (`YAMINE_LOG_MAX_BYTES`, one generation) before each write. A new
179
+ `doctor` disk-usage check warns past 100MB of state.
180
+
181
+ ### Fixed
182
+
183
+ - **Silent `:1355` URL fallback removed.** Privileged-port (443) bind
184
+ failure is now a hard error pointing at `yamine setup`, never a
185
+ booted app on `https://app.localhost:1355` that silently corrupts
186
+ downstream consumers of `YAMINE_URL`. The only port-suffixed URLs
187
+ are the ones you explicitly ask for (`proxy start -p 1355`).
188
+ - **Health probe `130+?` hang fixed.** The TLS probe's `connect` sat
189
+ outside the timeout: a TLS handshake against a foreign plain-HTTP
190
+ server blocked in `connect` for 60s+. Connect is now inside the
191
+ timeout, plain HTTP is tried first (our proxy answers plain HTTP via
192
+ byte-peeking even on the TLS port), and any HTTP response without our
193
+ header short-circuits as foreign — only silent servers wait for the
194
+ timeout.
195
+ - `start` dispatch was missing from the dispatcher despite being in
196
+ `SUBCOMMANDS`, so `yamine start` fell through to `run_named` with
197
+ "start" as an app name. The kamal-help append drifted to a 6-space
198
+ indent. Both are fixed and pinned by tests.
199
+ - `base64` declared as a runtime dependency (it left the default gems in
200
+ Ruby 3.4).
201
+ - Missing `require "optparse"` lost in the CLI split.
202
+
203
+ ### Tests — new coverage for this patch
204
+
205
+ - `start_test.rb` — help, fast-path vs. needs-setup branching, and the
206
+ non-interactive hard-error message.
207
+ - `setup_test.rb` — four-step orchestration (all-steps-stubbed), first-failure
208
+ abort with fix text, `--no-service` flag, and the three `ensure_proxy!`
209
+ hard-error paths (non-interactive, foreign port, spawn failure) plus
210
+ explicit-port URL honesty and responding-foreign-server fast classification.
211
+ - Pinned under `bundle exec rake test` (fast unit suite); no `test:e2e`
212
+ needed for these.
213
+
214
+
215
+ ## [0.1.0]
216
+
217
+ ### Added
218
+
219
+ - Initial release: explicit-run reverse proxy giving every Ruby app a
220
+ stable `https://<app>.localhost` URL.
221
+ - Zero-flag name inference (Rails module, gemspec, package.json, git
222
+ root, directory) with `yamine.json` overrides.
223
+ - `{variant}.{service}.{app}.{tld}` hostname composition; linked-worktree
224
+ branch prefixes; custom `--tld` including owned domains.
225
+ - Managed Rack boot on unix sockets (rackup/TCP fallback); run mode with
226
+ `PORT`/`YAMINE_URL` injection; `Procfile.dev` and static-site
227
+ framework detection.
228
+ - Local CA + per-host SNI certs (in-memory LRU), `trust`, `hosts sync`,
229
+ `doctor`, `prune`, `alias`, `get`, `kamal` snippet.
230
+ - Explicit `--port/--host` injection for port-ignoring CLIs (Jekyll,
231
+ Middleman, Bridgetown), skipped when the user already set a port.
232
+
233
+ ### Changed
234
+
235
+ - Rails module inference kebab-cases CamelCase and digit runs:
236
+ `Rails8Min` → `rails-8-min` (was `rails8min`).
237
+ - Monorepo `yamine.json` with an `apps:` map is discovered by walking
238
+ up from the package directory.
239
+ - `service: web` produces the bare `app.tld` (all other services prefix).
240
+ - Puma 8 command shape (positional `config.ru`; `--rackup` was removed
241
+ upstream) and spawned backends run unbundled so system/bundle puma is
242
+ reachable regardless of the invoking bundle.
243
+ - Boot failures include the backend log tail in the error message.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kaka Ruto
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,294 @@
1
+ # yamine
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/yamine.svg)](https://badge.fury.io/rb/yamine)
4
+
5
+ Stable named `.localhost` URLs for Ruby development. Gives every Ruby app
6
+ a stable `https://<app>.localhost` URL instead of a memorized port.
7
+ Zero runtime dependencies — Ruby stdlib only (`openssl`, `socket`).
8
+
9
+ ```bash
10
+ gem install yamine
11
+ yamine start # setup + boot in one go (or plain `yamine`)
12
+ yamine setup # once per machine: CA trust + port 443 + hosts + verify
13
+ cd ~/code/myapp && yamine
14
+ # -> https://myapp.localhost
15
+ ```
16
+
17
+
18
+ ## Why "yamine"?
19
+
20
+ **Yamine** (يمين, *yamīn*) is Arabic for "right hand" — the side of blessing
21
+ and good fortune, the hand you keep things close with. It sits next to
22
+ **Kamal** (كمال, *kamāl*, "completeness, perfection"): Kamal completes the
23
+ deploy, Yamine keeps the app at your right hand — local, close, yours.
24
+
25
+ And yes, if you follow football: *Yamine Kamal* is Lamine Yamal with the
26
+ names flipped. I'm a fan — the kid who nutmegs entire defenses at sixteen
27
+ is exactly the energy local dev should have. The local half of the game,
28
+ played the same way: fast, fearless, and fun to watch.
29
+
30
+ (For the curious: `yamine` is pronounced "ya-MEEN".)
31
+
32
+ ## The no-fallback promise
33
+
34
+ yamine never silently degrades to a `:<port>` URL. Clean
35
+ `https://<app>.localhost` requires the proxy on port 443; if 443
36
+ cannot be bound, you get a hard error pointing at `yamine setup`
37
+ — never a booted app on `https://app.localhost:1355` that silently
38
+ poisons OAuth callbacks, mailer hosts, and webhooks downstream.
39
+
40
+ The only port-suffixed URLs are the ones you explicitly ask for:
41
+ `yamine proxy start -p 1355` (CI/sandboxes where 443 is impossible).
42
+ There the suffix is honest, and `YAMINE_URL` carries it faithfully.
43
+
44
+ ## Port 443: one-time setup, then never again
45
+
46
+ Binding 443 is privileged, so yamine installs a **root-owned launchd
47
+ service** (macOS) or systemd unit (Linux) that binds 443 at boot — the
48
+ same model as puma-dev and portless. Installing it needs sudo **once per
49
+ machine**; after that, every `yamine` run in any project gets a clean
50
+ `https://<app>.localhost` with no elevation and no prompt.
51
+
52
+ **Human (interactive):** run setup once — it trusts the CA, installs the
53
+ service, syncs hosts, and verifies:
54
+
55
+ ```bash
56
+ yamine setup
57
+ ```
58
+
59
+ **Agent / CI (no TTY):** the same commands fail fast with guidance,
60
+ because sudo needs a terminal. To pre-provision a machine or image so
61
+ agents can install the service without a prompt, install the scoped
62
+ passwordless-sudo rules once (as an admin):
63
+
64
+ ```bash
65
+ yamine sudoers > /tmp/yamine.sudoers
66
+ sudo install -o root -g wheel -m 440 /tmp/yamine.sudoers /etc/sudoers.d/yamine # macOS
67
+ sudo install -o root -g root -m 440 /tmp/yamine.sudoers /etc/sudoers.d/yamine # Linux
68
+ ```
69
+
70
+ `yamine sudoers` prints rules scoped to yamine's own service
71
+ re-exec — the gem's exact ruby + bin path with the `service install
72
+ --internal` / `service uninstall --internal` subcommands — never a bare
73
+ interpreter. Re-run it after upgrading the gem if the install path
74
+ changes. To undo: `sudo rm /etc/sudoers.d/yamine`.
75
+
76
+ ## The one-file model
77
+
78
+ Every app declares `config/local.yml` (Kamal-style) — the single source
79
+ of truth for service name, proxy TLD/host, processes, and env:
80
+
81
+ ```yaml
82
+ service: myapp
83
+ proxy:
84
+ tld: localhost
85
+ processes:
86
+ web:
87
+ cmd: bundle exec puma -b tcp://127.0.0.1:$PORT config.ru
88
+ proxy: true
89
+ worker:
90
+ cmd: bundle exec sidekiq
91
+ proxy: false
92
+ ```
93
+
94
+ `yamine init` creates the file (migrating an existing Procfile);
95
+ Rails apps need no extra gem — yamine injects `RAILS_DEVELOPMENT_HOSTS`
96
+ so the proxied hostname is allowed automatically. `yamine`
97
+ then boots every process, assigns each a `$PORT`, injects
98
+ `YAMINE_URL`, registers routes for HTTP processes, supervises the
99
+ whole tree, and cleans up when one exits.
100
+
101
+ Variants are file overlays: `config/local.<variant>.yml` deep-merges on
102
+ top of `config/local.yml`, selected by `YAMINE_VARIANT` (Kamal's
103
+ destination pattern).
104
+
105
+ `.localhost` resolves to loopback natively in Chrome, Firefox, and Edge —
106
+ no DNS server, no `/etc/resolver`. Safari may need `yamine hosts sync`.
107
+
108
+ ## Hostname shape
109
+
110
+ ```
111
+ {variant}.{service}.{app}.{tld}
112
+ ```
113
+
114
+ | Axis | Example | Source |
115
+ |---|---|---|
116
+ | app | `myapp` | inferred, `--name`, `yamine.json`, `YAMINE_NAME` |
117
+ | service | `api.myapp` | `--service`, `YAMINE_SERVICE` (`web` stays bare) |
118
+ | variant | `fix-ui.myapp` | `--variant`, `YAMINE_VARIANT`, linked worktree branch |
119
+ | tld | `myapp.preview.example.com` | `--tld` (default `localhost`) |
120
+
121
+ Linked git worktrees get a branch prefix automatically
122
+ (`fix-ui.myapp.localhost`); the main checkout keeps the bare name.
123
+ Pass `--branch` (or `YAMINE_BRANCH=1`) to prefix by current branch
124
+ outside worktrees. `main`/`master`/detached HEAD never prefix.
125
+
126
+ ```bash
127
+ yamine # -> https://myapp.localhost
128
+ yamine --service api # -> https://api.myapp.localhost
129
+ yamine --variant demo # -> https://demo.myapp.localhost
130
+ yamine --tld preview.example.com # your own domain (OAuth parity)
131
+ ```
132
+
133
+ ## Commands
134
+
135
+ ```bash
136
+ yamine # infer name, boot app
137
+ yamine run -- <cmd> # run explicit command through proxy
138
+ yamine <name> <cmd> # explicit app name
139
+ yamine get <name> # print URL for cross-service wiring
140
+ yamine alias <name> <port> # static route (e.g. Docker)
141
+ yamine list [--json] # show active routes (+ backend liveness)
142
+ yamine status [--json] # show effective naming context here
143
+ yamine doctor [--json] # machine-readable health checks
144
+ yamine open [name] # open the app URL in a browser
145
+ yamine doctor # read-only health check (state, proxy, routes, DNS, CA)
146
+ yamine trust # add local CA to system trust store
147
+ yamine clean # remove state and hosts entries
148
+ yamine prune # remove stale routes
149
+ yamine stop # stop this app's backend + routes
150
+ yamine restart # touch tmp/restart.txt (managed apps reboot)
151
+ yamine log [-f] [n] # tail (or follow) this app's backend log
152
+ yamine proxy start|stop # control the proxy
153
+ yamine service install|status|uninstall # root-owned OS startup service
154
+ yamine hosts sync|clean # manage /etc/hosts entries
155
+ yamine kamal <variant> # preview-deploy snippet for Kamal
156
+ ```
157
+
158
+ Child processes receive `YAMINE_URL` (the stable URL — use it for
159
+ OAuth callbacks, mailer hosts, webhook URLs), `PORT`, and `HOST`.
160
+
161
+ ## Frameworks
162
+
163
+ Rails and bare Rack (`config.ru`) boot managed on a unix socket (Puma
164
+ when available; `rackup` on TCP otherwise). `Procfile.dev`/`bin/dev`,
165
+ Jekyll, Bridgetown, and Middleman run in run mode with `PORT` injected
166
+ (port-ignoring CLIs get explicit `--port/--host` flags). Anything else:
167
+ `yamine run -- <cmd>`.
168
+
169
+ ```bash
170
+ yamine --proc web # boot a specific Procfile process
171
+ yamine --proc worker # (first line is the default)
172
+ ```
173
+
174
+ Procfile lines that are compound (`&&`, `||`, `|`, `;`) are refused with
175
+ guidance rather than silently mis-injected.
176
+
177
+ For Rails integration (hosts, Action Cable origins, Procfile rewrite,
178
+ generators) — deprecated; core covers Rails now.
179
+
180
+ ## WebSockets
181
+
182
+ Action Cable and any Rack hijack-based WebSocket server work through the
183
+ proxy: HTTP/1.1 `Upgrade` requests are byte-forwarded to the backend
184
+ after header rewriting, and the tunnel stays raw for the life of the
185
+ connection (verified end-to-end: RFC 6455 handshake + frame echo).
186
+
187
+ ## Machine-readable output
188
+
189
+ `list`, `status`, and `doctor` accept `--json` with stable keys for
190
+ agents and scripts (`DevUrl` in ask-ruby-harness consumes the same data
191
+ in-process). Hostnames that fall outside the configured TLDs get a bare
192
+ 404 naming nothing — route names never leak to foreign hosts
193
+ (DNS-rebinding boundary).
194
+
195
+ ## Log rotation
196
+
197
+ `proxy.log` and per-app backend logs rotate at 5MB
198
+ (`YAMINE_LOG_MAX_BYTES`), keeping one generation. `doctor` warns
199
+ when the state dir passes 100MB.
200
+
201
+ ## Supervision
202
+
203
+ Managed apps are supervised by the proxy daemon, not the CLI:
204
+
205
+ - idle backends stop after 15 minutes (`YAMINE_IDLE_TIMEOUT`
206
+ seconds; `0` disables) and boot transparently on the next request
207
+ - touching `tmp/restart.txt` stops the backend; next request reboots it
208
+ - crashed backends are detected and rebooted on the next request
209
+ - daemon shutdown stops every supervised backend (no orphans)
210
+
211
+ Run-mode (TCP) routes and static aliases are never supervised.
212
+
213
+ ## Ask ecosystem integration
214
+
215
+ | Gem | How yamine helps |
216
+ |---|---|
217
+ | `ask-rails` | yamine core injects `RAILS_DEVELOPMENT_HOSTS`; Cable origins + helpers live in the deprecated yamine-rails |
218
+ | `ask-rails-harness` | Its 9 Rails tools (routes, models, DB, logs) run against the app the proxy serves; `DevUrl` gives the agent the stable URL instead of a guessed port |
219
+ | `ask-app-server` | The JSON-RPC/stdio session host sits behind `https://api.<app>.localhost`; editor/IDE clients use `yamine get` output |
220
+ | `ask-mcp` | MCP servers get named URLs per service (`mcp.<app>.localhost`), no port coordination across servers |
221
+ | `ask-skills` | Ships the `yamine` skill (auto-discovered): boot via `yamine`, wire via `get`, callbacks from `YAMINE_URL` |
222
+ | `ask-ruby-harness` | `DevUrl` tool: structured `list`/`get` for agents, audit-logged like every other tool |
223
+
224
+ What we do differently from Kamal for local dev: Kamal + kamal-proxy
225
+ own production (Let's Encrypt, zero-downtime deploys, multi-host).
226
+ yamine never serves prod — but the variant slug is shared, so
227
+ `fix-ui.myapp.localhost` locally and `myapp-fix-ui.preview.example.com`
228
+ in staging (via `yamine kamal fix-ui`) are the same branch everywhere.
229
+
230
+ ## Prior art
231
+
232
+ Same problem, three generations — yamine borrows from all of them:
233
+
234
+ - **Pow** (2011–2017, macOS-only Rack): the ergonomics — zero-config
235
+ names, `tmp/restart.txt`, `.powrc` env loading. Left behind: Nack
236
+ workers, firewall forwarding, HTTP-only, unmaintained.
237
+ - **puma-dev** (Go, macOS/Linux): the engine semantics — Puma on unix
238
+ sockets, lazy boot, idle kill, restart.txt watching, in-memory dynamic
239
+ TLS, per-route status. Kept as behavior, reimplemented in Ruby.
240
+ - **portless** (Node 24, any stack): the agent interface — explicit run
241
+ ownership, `PORTLESS_URL`-style env contract, `get/doctor/prune`,
242
+ worktree prefixes, custom-TLD OAuth parity, SKILL.md pattern.
243
+
244
+ Build vs borrow decision: yamine is pure Ruby (stdlib + base64),
245
+ not a wrapper around puma-dev's Go core. Rationale: zero-toolchain
246
+ distribution (`gem install`, no Go/Node), the ask-core zero-dependency
247
+ philosophy, and full control over the agent surface (route store,
248
+ supervision, skills). puma-dev's semantics were ported, not its binary.
249
+
250
+ ## Non-goals (deliberate)
251
+
252
+ - **HTTP/2.** Ruby dev servers serve a handful of requests, not Vite's
253
+ hundreds of unbundled files — the multiplexing win doesn't apply, and
254
+ ALPN/HPACK/stream state would triple the proxy's auditable surface.
255
+ Revisit only on benchmarked HMR latency. (Consequence: no HTTP/2
256
+ extended-CONNECT bridging; browsers never negotiate h2 here, so plain
257
+ Upgrade tunneling covers Action Cable fully.)
258
+ - **LAN/mDNS and Tailscale/ngrok tunnels.** mDNS behaves differently on
259
+ every network; tunnels need third-party CLIs, auth state, and accounts.
260
+ The 95% "show this branch to someone" case is covered by the `kamal`
261
+ preview-deploy snippet on real infrastructure instead of a laptop
262
+ tunnel. Kamal owns remote access; yamine owns local naming.
263
+ - **Production serving.** The proxy binds loopback only, the CA is
264
+ self-signed, and there is no request buffering, rate limiting, or
265
+ access control. Anything real goes through Kamal + kamal-proxy.
266
+
267
+ ## Development
268
+
269
+ ```bash
270
+ bundle install
271
+ bundle exec rake test
272
+ ```
273
+
274
+ ## Non-goals (deliberate)
275
+
276
+ - **HTTP/2.** Ruby dev servers serve a handful of requests, not Vite's
277
+ hundreds of unbundled files — the multiplexing win doesn't apply, and
278
+ ALPN/HPACK/stream state would triple the proxy's auditable surface.
279
+ - **LAN/mDNS or tunneled sharing.** mDNS behaves differently on every
280
+ network; third-party tunnels need CLIs, auth state, and accounts.
281
+ The `kamal` preview-deploy snippet covers "show this branch to
282
+ someone" on real infrastructure instead.
283
+ - **Production serving.** The proxy binds loopback only, the CA is
284
+ self-signed, and there is no buffering or rate limiting.
285
+
286
+
287
+ The `yamine-apps` fixture fleet (sibling checkout) exercises
288
+ detection, inference, and boot across Rails variants, Roda, Sinatra,
289
+ bare Rack, Jekyll, compound Procfiles, and a monorepo. CI runs the
290
+ fixture sweep automatically.
291
+
292
+ ## License
293
+
294
+ MIT
data/bin/yamine ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/yamine"
5
+
6
+ exit Yamine::CLI.run(ARGV)