yamine 0.21.2 → 0.22.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 618e529eeea12aa43397e084455136a4062bd55a546e904a1cb4170e3fc1466f
4
- data.tar.gz: aa598da83330345ca84a60b168539262aa516df7440d6d91611b5cc853c584ec
3
+ metadata.gz: 1f20b021d3a702ab4aef776ac24e10683b12c1b4ba58b7ad4e5b11b91316eb78
4
+ data.tar.gz: 4ff8a2e59f075f38aa81d084d9aea45041346790aefd687ef2fc08669bc34737
5
5
  SHA512:
6
- metadata.gz: 848bb6e6cced84dc132e38dc2ce4828c3514b4fca2c4596e796e9e938b1b2f23d164aadea56e14aed9e8c13d7f732723ede5b377f443db068f74bfd7a9e88410
7
- data.tar.gz: 453dd9432df8527eace777b7894abf8540b60f103243ce4917593cd021c38506a0aa76614ca0eb926caea5ebfca6f25177729148e5342f22e1b571f7571d104a
6
+ metadata.gz: 2e2542be9656d87375b9ab29ac2862f33b2be289c7932aa99f2f29abf3b2a47f40e24623f2ab8b902021e6b42fe4ed48ebe7dfc4a86a3184ddf4f859cb359d40
7
+ data.tar.gz: 85d76ec111ee796d23b89879d414bf5a248504f58983a0064c3d43ffaa88d532a0f3b5d6542f21b09d2fba5acb57f20e673fcb5f7d13490108f321adbf1eb896
data/CHANGELOG.md CHANGED
@@ -2,6 +2,176 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.22.1] - 2026-09-29
6
+
7
+ ### Fixed
8
+
9
+ - **macOS trusts the local CA now, instead of merely filing it.** A
10
+ non-elevated `yamine trust` added the certificate without naming any
11
+ policies, so macOS recorded a trust-store entry with no settings at
12
+ all: the CA sat in the login keychain trusted for nothing, and every
13
+ browser answered `ERR_CERT_AUTHORITY_INVALID` on every
14
+ `https://*.localhost` route. The add now passes `-p ssl -p basic` —
15
+ the same thing Keychain Access writes for Secure Sockets Layer + X.509
16
+ Basic — and `yamine trust` re-reads the trust store afterwards rather
17
+ than believing the exit status, so an add that recorded nothing is a
18
+ failure that names the exact `security add-trusted-cert` to run
19
+ instead of a success the browser will contradict.
20
+ - **A CA that is in the keychain but untrusted repairs itself.** The
21
+ check that decided whether to trust compared fingerprints alone, so
22
+ once that bare certificate was in a keychain every later boot
23
+ concluded it was trusted and never touched it again — the check that
24
+ should have caught the broken state was the one hiding it. It now
25
+ needs both halves: the certificate in a keychain *and* a trust
26
+ setting recorded for its fingerprint. A machine left with a
27
+ policy-less entry therefore self-heals on the next `yamine trust` or
28
+ proxy boot, and one whose trust store cannot be read is left alone
29
+ rather than re-adding on every boot.
30
+ - **A proxy running as a normal user trusts the CA too.** Trust used to
31
+ be attempted only when the proxy was root, so a first run without
32
+ `sudo` never tried and installed a CA that could not work. Every proxy
33
+ now ensures trust, elevated or not, and reports a failed attempt
34
+ rather than swallowing it. `YAMINE_SKIP_CA_TRUST` is the escape
35
+ hatch for machines whose CA arrives by MDM or a hand-run
36
+ `security add-trusted-cert`.
37
+ - **`yamine doctor` names a CA the OS does not trust.** The recorded
38
+ marker says yamine trusted this certificate; it says nothing about
39
+ whether macOS agreed. `doctor` asks the OS too, so a CA that is
40
+ installed but untrusted — the state where every route fails TLS —
41
+ reads as "CA is installed but macOS does not trust it" and points at
42
+ `yamine trust` instead of passing as healthy.
43
+ ## [0.22.0] - 2026-09-29
44
+
45
+ ### Added
46
+
47
+ - **`yamine start --detach` — boot in the background and get your prompt
48
+ back.** An agent (or a script) that needs an app running had one
49
+ option: block, or reach for `nohup` — which leaves a process tree
50
+ whose owner the first Ctrl-C cannot reach. `--detach` forks the boot:
51
+ the child takes its own session, records its pid in
52
+ `~/.yamine/start-<hostname>.pid`, sends its own output to
53
+ `~/.yamine/start-<hostname>.log`, boots, keeps the tree supervised and
54
+ outlives the shell that started it. The parent waits for the app to
55
+ answer, prints the URL, that pid and the log path, and exits 0.
56
+ The boot happens in the child on purpose: `add_route` records
57
+ `Process.pid`, and `load_routes` prunes any route whose pid is dead, so
58
+ a parent that registered the routes and exited would have its own route
59
+ pruned on the next read and the proxy would 503 an app that is running
60
+ perfectly well. It is idempotent — a tree already running for the
61
+ directory (detached, or foreground) is reported, not started again —
62
+ and a boot that never becomes healthy exits 1 with the log instead of
63
+ a tree that is not there. `--json` gets the same three facts
64
+ as a payload. `--no-wait` is ignored with `--detach`: returning before
65
+ the app answers is the lie this flag exists to stop.
66
+
67
+ ### Fixed
68
+
69
+ - **`yamine list` and `yamine get --all` report the app, not the CLI
70
+ that started it.** A route's `pid` is the yamine process that
71
+ registered it, and it outlives the app it booted, so `alive_state` was
72
+ asking the wrong process whether the app was alive: every crashed
73
+ backend read as `running`, in the one command an agent would reach for
74
+ to find out. Liveness now comes from the sidecar the boot already
75
+ writes (`backend-<hostname>.pid`, the same file `yamine stop` reads),
76
+ and the vocabulary says what it knows:
77
+ `running` (the app's process is there), `backend-gone` (the CLI is
78
+ still up, the app it booted is not), `owner-gone` (nothing is there
79
+ and no app pid was recorded), `unknown` (no app pid recorded, and the
80
+ route's own process is — a route written before sidecars existed, or
81
+ by a boot that died between registering the route and writing the
82
+ file: `unknown`, never "down", so no pre-existing route reads as
83
+ broken after an upgrade), and `reachable` / `unreachable` for a static
84
+ alias, which names no process and keeps reporting the probe.
85
+ `list --json` gained `backend_pid` beside `pid` so the state and the
86
+ process it is about travel together, and the human line prints the
87
+ backend's pid rather than the owner's.
88
+ - **`yamine restart` works for the apps yamine started.** Supervision
89
+ required `kind == "socket"`, and `yamine start` registers `kind
90
+ "tcp"`, so a `yamine start` tree was invisible to the daemon:
91
+ `yamine restart` touched `tmp/restart.txt`, printed "managed app
92
+ restarts on next request", and nothing was watching the file. Any
93
+ route carrying a spec is now watched, so restart.txt and crash
94
+ detection reach both kinds. Two things that were broken *inside* the
95
+ socket case went with it: the restart baseline was re-derived while
96
+ `restart.txt` did not exist, so the *first* `yamine restart` an app
97
+ ever got was absorbed as the new baseline and did nothing; and the
98
+ "restarting" latch was never cleared, so a route was supervised for
99
+ exactly one event in the life of the daemon. The kill now signals the
100
+ process group when the pid leads one, because a tcp route's sidecar
101
+ names the `sh -c` shell the boot wrapped the app in.
102
+ - **A child that dies no longer ends `yamine start` with exit 0.**
103
+ `supervise_tree` exited 0 with the routes already removed and the rest
104
+ of the tree being killed, so a zero read as "the app came up" to
105
+ anything scripted — which then blamed the app for the 503 it was
106
+ about to get. It exits 1, the same code `--wait` already uses for a
107
+ boot that never became healthy, and the message names the process, its
108
+ exit status (or the signal that took it down — a reaped child is the
109
+ only place that answer exists, so the reaper keeps it) and the tail of
110
+ the app log. `--json` gets the same as
111
+ `{"ok": false, "error": "child-exited", …}`. `yamine stop`'s
112
+ 0/2/3/4 are untouched: those answer "what did the stop do", and this
113
+ answers "is the app up".
114
+ - **`yamine restart` no longer promises a reboot it cannot perform.**
115
+ A managed socket app is rebooted on the next request; a `yamine start`
116
+ app is stopped and has to be started again, because the daemon cannot
117
+ rebuild a tcp backend (its port is a free port chosen at boot and the
118
+ route records no command to re-run). The command and the daemon's own
119
+ events now say which one you have.
120
+ - **A boot that started nothing no longer reports `ready:` and hangs.**
121
+ A process whose `cmd` is a compound line is refused with an error and
122
+ leaves the spawn plan empty; the boot then printed `ready:` and sat in
123
+ the supervision loop for ever with nothing to watch — a hang that looks
124
+ exactly like a healthy start. It exits 1 instead, naming the error
125
+ above it.
126
+
127
+ ### Changed
128
+
129
+ - **A `yamine start` app is watched by the proxy daemon, but is not
130
+ idle-killed.** This is the one visible consequence of the supervision
131
+ fix above, so it is called out rather than buried: previously *no*
132
+ `yamine start` app was ever idle-killed, and now restart.txt and crash
133
+ detection work — but the idle clock still does not touch them, because
134
+ idle-kill is the half of puma-dev that depends on the other half
135
+ ("stopped, boots on next request"), and the daemon cannot keep that
136
+ promise for a tcp route. Killing one would take a developer's app down
137
+ for an afternoon and leave a 503 where it was. Set `YAMINE_IDLE_TCP=1`
138
+ for the puma-dev behaviour on `yamine start` apps, with the same
139
+ `YAMINE_IDLE_TIMEOUT` clock (15 minutes by default; that is the
140
+ supervisor's clock, distinct from the proxy's own
141
+ `YAMINE_PROXY_IDLE_TIMEOUT`). For the same reason `yamine proxy stop`
142
+ no longer stops `yamine start` apps along with the socket ones the
143
+ daemon booted: those have a supervising process of their own.
144
+
145
+ ### Fixed
146
+
147
+ - **A slow app and a dead app no longer come back as the same useless
148
+ 502.** Waiting for the backend's response *head* and waiting for body
149
+ bytes shared one clock, and `read_head` swallowed its own timeout into
150
+ the same `[nil, ...]` a dead backend returns — so `pipe_request` could
151
+ not tell them apart and every one of them got the same 60-byte page,
152
+ "The target app is not responding." A font request that took 60,068ms
153
+ came back with no hostname, no target, no owner, no directory and no
154
+ next step. The head now has its own budget
155
+ (`YAMINE_PROXY_HEAD_TIMEOUT`, default 60s — the same bound it already
156
+ got, so nothing that works today starts failing), the body keeps
157
+ `YAMINE_PROXY_IDLE_TIMEOUT` untouched, and a silent backend is reported
158
+ as what it is: *the backend at 127.0.0.1:3000 accepted the connection,
159
+ then sent no response for 60 seconds*, with the owning agent, the app's
160
+ own directory (`spec.dir`, not the proxy's cwd — inside a worktree that
161
+ is the wrong checkout), the path to its `log/development.log`, and the
162
+ `cd <dir> && yamine start` that fixes it. Machines get the same split
163
+ in a header: `x-yamine-error: backend-refused` or `backend-silent`.
164
+ A body clock that was tuned tight for one and a head clock that is not
165
+ are no longer the same dial, so a streaming response that goes quiet
166
+ between bytes still runs indefinitely.
167
+ - **A request that was merely slow now gets a second attempt.** One
168
+ retry, and only where a retry cannot duplicate work: a refused dial
169
+ never reached the app, so any method replays; a head timeout means the
170
+ head *did* arrive, so only bodiless GET, HEAD and OPTIONS replay. A
171
+ GET that takes 61 seconds to answer now serves instead of 502ing. A
172
+ POST never replays — the body is already consumed off the client socket
173
+ and cannot be resent faithfully, and a duplicated message is worse than
174
+ a slow page.
5
175
  ## [0.21.2] — 2026-09-29
6
176
 
7
177
  ### Fixed
data/README.md CHANGED
@@ -355,11 +355,12 @@ as the app answering 404. `yamine status` reports which mode an app is in.
355
355
 
356
356
  ```bash
357
357
  yamine # boot app (waits until healthy, then supervises)
358
+ yamine start --detach # boot in the background; returns once healthy, prints url/pid/log
358
359
  yamine start --no-wait # fire-and-forget (register routes immediately)
359
360
  yamine start --json # machine-readable wait result (--wait default)
360
361
  yamine get <name> # print URL for cross-service wiring
361
362
  yamine alias <name> <port> # static route (e.g. Docker)
362
- yamine list [--json] # show active routes (+ backend liveness)
363
+ yamine list [--json] # show active routes (+ the APP's liveness)
363
364
  yamine status [--json] # show effective naming context here
364
365
  yamine doctor [--json] # machine-readable health checks
365
366
  yamine open [name] # open the app URL in a browser
@@ -369,7 +370,7 @@ yamine prune # remove stale routes
369
370
  yamine db list|create|drop|describe # per-worktree databases (multi-database aware)
370
371
  yamine worktree list|add|remove|clean # worktree lifecycle
371
372
  yamine stop # stop this app's backend + routes
372
- yamine restart # touch tmp/restart.txt (managed apps reboot)
373
+ yamine restart # touch tmp/restart.txt (a supervised app's backend is stopped)
373
374
  yamine log [-F] [n] # tail (or follow) log/development.log
374
375
  yamine proxy start|stop # control the proxy
375
376
  yamine service install|status|uninstall # root-owned OS startup service
@@ -413,7 +414,19 @@ in-process). Hostnames that fall outside the configured TLDs get a bare
413
414
  (healthcheck path when declared, TCP accept otherwise). On failure it
414
415
  exits 1 with the failed process, its phase, and the tail of its own log
415
416
  — no guessing, no polling, no half-booted routes. `--no-wait` keeps the
416
- old fire-and-forget path.
417
+ old fire-and-forget path. A child that dies later ends the run the same
418
+ way: exit 1, naming the process, its exit status (or the signal that
419
+ took it down) and the tail of its log. Zero means the app is up.
420
+
421
+ `yamine start --detach` boots the same tree into the background and
422
+ returns once it is healthy, printing the URL, the pid that owns the tree
423
+ and its log path under the state dir (`start-<hostname>.pid` /
424
+ `start-<hostname>.log`). The boot runs in the child, so that pid is the
425
+ one recorded in `routes.json` — the route outlives the command. It is
426
+ idempotent (a tree already running for the directory is reported, not
427
+ restarted), it ignores `--no-wait` (returning before the app answers is
428
+ the thing it exists to prevent), and `--json` returns
429
+ `{ok, url, pid, log_path, started}`.
417
430
 
418
431
  ## Log rotation
419
432
 
@@ -424,15 +437,29 @@ lose the tail. `doctor` warns when the state dir passes 100MB.
424
437
 
425
438
  ## Supervision
426
439
 
427
- Managed apps are supervised by the proxy daemon, not the CLI:
428
-
429
- - idle backends stop after 15 minutes (`YAMINE_IDLE_TIMEOUT`
430
- seconds; `0` disables) and boot transparently on the next request
431
- - touching `tmp/restart.txt` stops the backend; next request reboots it
432
- - crashed backends are detected and rebooted on the next request
433
- - daemon shutdown stops every supervised backend (no orphans)
434
-
435
- Run-mode (TCP) routes and static aliases are never supervised.
440
+ Apps are supervised by the proxy daemon, not the CLI. A route is
441
+ supervised when it names a directory to boot from (`spec.dir`), which
442
+ is every app yamine boots — managed socket apps *and* `yamine start`
443
+ trees. Static aliases are not.
444
+
445
+ - touching `tmp/restart.txt` stops the backend. A managed socket app
446
+ then reboots on the next request; a `yamine start` app has to be
447
+ started again (`yamine restart` says which one you have)
448
+ - crashed backends are detected, and a managed app is rebooted on the
449
+ next request
450
+ - a managed app idle-kills after 15 minutes (`YAMINE_IDLE_TIMEOUT`
451
+ seconds; `0` disables) and boots transparently on the next request
452
+ - daemon shutdown stops the backends the daemon booted (no orphans)
453
+
454
+ A `yamine start` app is **watched but not idle-killed**, and is not
455
+ stopped when the proxy stops. Idle-kill is the half of puma-dev that
456
+ depends on the other half — "stopped, boots on next request" — and the
457
+ daemon can only keep that promise for a socket app: a tcp route's port
458
+ is a free port chosen at boot and the route records no command to
459
+ re-run, so nothing could bring it back. Set `YAMINE_IDLE_TCP=1` to get
460
+ the puma-dev behaviour for `yamine start` apps too (same
461
+ `YAMINE_IDLE_TIMEOUT` clock — the supervisor's, distinct from the
462
+ proxy's own `YAMINE_PROXY_IDLE_TIMEOUT`).
436
463
 
437
464
  ## Ask ecosystem integration
438
465
 
@@ -36,6 +36,7 @@ no extra gem — the proxied hostname is allowed automatically via
36
36
  yamine start # setup if needed, then boot every process (waits until healthy)
37
37
  yamine start --no-wait # fire-and-forget (register routes immediately)
38
38
  yamine start --json # machine-readable wait result (--wait default)
39
+ yamine start --detach # boot in the background; returns once healthy, prints url/pid/log
39
40
  yamine # same as start
40
41
  yamine stop # stop this app's backend + routes
41
42
  yamine status # show service, processes, and URLs
@@ -44,7 +45,16 @@ yamine log [-F] # tail log/development.log (every process)
44
45
 
45
46
  `$PORT` and `YAMINE_URL` are injected per process; HTTP processes get
46
47
  stable URLs, background ones are supervised without routes. A process
47
- that exits cleans up the whole tree.
48
+ that exits cleans up the whole tree — and ends the run with exit 1,
49
+ naming that process, its exit status and the tail of its log. Zero from
50
+ `yamine start` means the app is up.
51
+
52
+ `--detach` is the one to use from a tool: it forks the boot, so the
53
+ command returns with the URL, the pid that owns the tree (recorded in
54
+ `routes.json`, so the route outlives the command) and that tree's log
55
+ path under `~/.yamine/`. Run it again and it reports the running tree
56
+ instead of starting a second one; `yamine stop` stops it. Do not reach
57
+ for `nohup` — the tree yamine starts is the one `yamine stop` can reach.
48
58
 
49
59
  The boot narrates every phase — deps, db, schema, and each process's
50
60
  healthcheck — one line per phase, so a slow boot is never a black box:
@@ -193,6 +203,15 @@ yamine prune # clear stale routes from crashed sessions
193
203
  yamine start --json # boot readiness payload (pass/fail + log tail)
194
204
  ```
195
205
 
206
+ `yamine list` reports the state of the **app**, not of the yamine
207
+ process that started it: `running`, `backend-gone` (the app died, the
208
+ process that booted it is still there), `owner-gone` (both gone),
209
+ `unknown` (no backend pid recorded — nothing is known, it is not a
210
+ failure), and `reachable` / `unreachable` for a `yamine alias`, which
211
+ points at a port rather than a process. `yamine restart` stops the
212
+ backend; a managed app reboots on the next request, a `yamine start` app
213
+ has to be started again.
214
+
196
215
  If a hostname does not resolve — or boot/doctor report it as not in
197
216
  `/etc/hosts` and you use clients that read only that file (CGO-disabled
198
217
  Go binaries): `yamine hosts sync`. If the browser warns about TLS: