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 +7 -0
- data/CHANGELOG.md +243 -0
- data/LICENSE +21 -0
- data/README.md +294 -0
- data/bin/yamine +6 -0
- data/lib/ask/skills/yamine/SKILL.md +113 -0
- data/lib/yamine/certs.rb +177 -0
- data/lib/yamine/cli/boot.rb +247 -0
- data/lib/yamine/cli/context.rb +104 -0
- data/lib/yamine/cli/routes.rb +255 -0
- data/lib/yamine/cli/system.rb +768 -0
- data/lib/yamine/cli.rb +108 -0
- data/lib/yamine/command.rb +28 -0
- data/lib/yamine/config.rb +394 -0
- data/lib/yamine/doctor.rb +133 -0
- data/lib/yamine/errors.rb +18 -0
- data/lib/yamine/framework.rb +75 -0
- data/lib/yamine/hostname.rb +44 -0
- data/lib/yamine/hosts.rb +86 -0
- data/lib/yamine/inference.rb +142 -0
- data/lib/yamine/log.rb +59 -0
- data/lib/yamine/ownership.rb +53 -0
- data/lib/yamine/ports.rb +45 -0
- data/lib/yamine/procfile.rb +137 -0
- data/lib/yamine/proxy.rb +489 -0
- data/lib/yamine/proxy_control.rb +222 -0
- data/lib/yamine/resolver.rb +92 -0
- data/lib/yamine/route_store.rb +148 -0
- data/lib/yamine/runner.rb +243 -0
- data/lib/yamine/sanitize.rb +41 -0
- data/lib/yamine/supervisor.rb +242 -0
- data/lib/yamine/trust.rb +131 -0
- data/lib/yamine/variant.rb +134 -0
- data/lib/yamine/version.rb +5 -0
- data/lib/yamine.rb +37 -0
- metadata +137 -0
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
|
+
[](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
|