yamine 0.21.1 → 0.22.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e920f5f6d29a92e0a6e81ca671bccf62e0a264ec453f9b5bc4d42d4e56980d3
4
- data.tar.gz: cc06342930ee7c882cf30fd2818f1a60495c8a59e44ec6779cc666f569493ac0
3
+ metadata.gz: 69848a3d38cd9997ca18e52ea892aa57962524e0cd41df16199a651409e86b76
4
+ data.tar.gz: aebe3dd18c4bab95865491850bb940ca9a7e42c83f53d851b4d3ddd691f80305
5
5
  SHA512:
6
- metadata.gz: 4164dabf9bd5537023ad59d426a958a5e284a90f0226cc83e0533cf635edf0c65acc16dd311d58a88df83d7abdcdfb92034703ecda06d89af08bb5ec96c73635
7
- data.tar.gz: 1e3af2b55e54885fe8aeed98856c4371eab280ec981cd2644ebb75aba0206752ae44307c0252f51188a778426efbb882ef2285c63f142def39f4e876f77302d6
6
+ metadata.gz: 7c1bb5ef1104c29a53c8e4274a5b4a42f7231f895f40bb7d12defc97dee72232944340c24b4bec62b9a736d29bdab01d975b32cf6e1af7aaf7f1b643ec0ac602
7
+ data.tar.gz: 66ef5d87789c27a94994c0d24f1567c1a762d592ecbfb00b3d95db8a7f538f3c6868be3703a9870a88d0101aabb37dcfaba8f6db85b2e4a74799b4a4381c7280
data/CHANGELOG.md CHANGED
@@ -2,6 +2,172 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.22.0] - 2026-09-29
6
+
7
+ ### Added
8
+
9
+ - **`yamine start --detach` — boot in the background and get your prompt
10
+ back.** An agent (or a script) that needs an app running had one
11
+ option: block, or reach for `nohup` — which leaves a process tree
12
+ whose owner the first Ctrl-C cannot reach. `--detach` forks the boot:
13
+ the child takes its own session, records its pid in
14
+ `~/.yamine/start-<hostname>.pid`, sends its own output to
15
+ `~/.yamine/start-<hostname>.log`, boots, keeps the tree supervised and
16
+ outlives the shell that started it. The parent waits for the app to
17
+ answer, prints the URL, that pid and the log path, and exits 0.
18
+ The boot happens in the child on purpose: `add_route` records
19
+ `Process.pid`, and `load_routes` prunes any route whose pid is dead, so
20
+ a parent that registered the routes and exited would have its own route
21
+ pruned on the next read and the proxy would 503 an app that is running
22
+ perfectly well. It is idempotent — a tree already running for the
23
+ directory (detached, or foreground) is reported, not started again —
24
+ and a boot that never becomes healthy exits 1 with the log instead of
25
+ a tree that is not there. `--json` gets the same three facts
26
+ as a payload. `--no-wait` is ignored with `--detach`: returning before
27
+ the app answers is the lie this flag exists to stop.
28
+
29
+ ### Fixed
30
+
31
+ - **`yamine list` and `yamine get --all` report the app, not the CLI
32
+ that started it.** A route's `pid` is the yamine process that
33
+ registered it, and it outlives the app it booted, so `alive_state` was
34
+ asking the wrong process whether the app was alive: every crashed
35
+ backend read as `running`, in the one command an agent would reach for
36
+ to find out. Liveness now comes from the sidecar the boot already
37
+ writes (`backend-<hostname>.pid`, the same file `yamine stop` reads),
38
+ and the vocabulary says what it knows:
39
+ `running` (the app's process is there), `backend-gone` (the CLI is
40
+ still up, the app it booted is not), `owner-gone` (nothing is there
41
+ and no app pid was recorded), `unknown` (no app pid recorded, and the
42
+ route's own process is — a route written before sidecars existed, or
43
+ by a boot that died between registering the route and writing the
44
+ file: `unknown`, never "down", so no pre-existing route reads as
45
+ broken after an upgrade), and `reachable` / `unreachable` for a static
46
+ alias, which names no process and keeps reporting the probe.
47
+ `list --json` gained `backend_pid` beside `pid` so the state and the
48
+ process it is about travel together, and the human line prints the
49
+ backend's pid rather than the owner's.
50
+ - **`yamine restart` works for the apps yamine started.** Supervision
51
+ required `kind == "socket"`, and `yamine start` registers `kind
52
+ "tcp"`, so a `yamine start` tree was invisible to the daemon:
53
+ `yamine restart` touched `tmp/restart.txt`, printed "managed app
54
+ restarts on next request", and nothing was watching the file. Any
55
+ route carrying a spec is now watched, so restart.txt and crash
56
+ detection reach both kinds. Two things that were broken *inside* the
57
+ socket case went with it: the restart baseline was re-derived while
58
+ `restart.txt` did not exist, so the *first* `yamine restart` an app
59
+ ever got was absorbed as the new baseline and did nothing; and the
60
+ "restarting" latch was never cleared, so a route was supervised for
61
+ exactly one event in the life of the daemon. The kill now signals the
62
+ process group when the pid leads one, because a tcp route's sidecar
63
+ names the `sh -c` shell the boot wrapped the app in.
64
+ - **A child that dies no longer ends `yamine start` with exit 0.**
65
+ `supervise_tree` exited 0 with the routes already removed and the rest
66
+ of the tree being killed, so a zero read as "the app came up" to
67
+ anything scripted — which then blamed the app for the 503 it was
68
+ about to get. It exits 1, the same code `--wait` already uses for a
69
+ boot that never became healthy, and the message names the process, its
70
+ exit status (or the signal that took it down — a reaped child is the
71
+ only place that answer exists, so the reaper keeps it) and the tail of
72
+ the app log. `--json` gets the same as
73
+ `{"ok": false, "error": "child-exited", …}`. `yamine stop`'s
74
+ 0/2/3/4 are untouched: those answer "what did the stop do", and this
75
+ answers "is the app up".
76
+ - **`yamine restart` no longer promises a reboot it cannot perform.**
77
+ A managed socket app is rebooted on the next request; a `yamine start`
78
+ app is stopped and has to be started again, because the daemon cannot
79
+ rebuild a tcp backend (its port is a free port chosen at boot and the
80
+ route records no command to re-run). The command and the daemon's own
81
+ events now say which one you have.
82
+ - **A boot that started nothing no longer reports `ready:` and hangs.**
83
+ A process whose `cmd` is a compound line is refused with an error and
84
+ leaves the spawn plan empty; the boot then printed `ready:` and sat in
85
+ the supervision loop for ever with nothing to watch — a hang that looks
86
+ exactly like a healthy start. It exits 1 instead, naming the error
87
+ above it.
88
+
89
+ ### Changed
90
+
91
+ - **A `yamine start` app is watched by the proxy daemon, but is not
92
+ idle-killed.** This is the one visible consequence of the supervision
93
+ fix above, so it is called out rather than buried: previously *no*
94
+ `yamine start` app was ever idle-killed, and now restart.txt and crash
95
+ detection work — but the idle clock still does not touch them, because
96
+ idle-kill is the half of puma-dev that depends on the other half
97
+ ("stopped, boots on next request"), and the daemon cannot keep that
98
+ promise for a tcp route. Killing one would take a developer's app down
99
+ for an afternoon and leave a 503 where it was. Set `YAMINE_IDLE_TCP=1`
100
+ for the puma-dev behaviour on `yamine start` apps, with the same
101
+ `YAMINE_IDLE_TIMEOUT` clock (15 minutes by default; that is the
102
+ supervisor's clock, distinct from the proxy's own
103
+ `YAMINE_PROXY_IDLE_TIMEOUT`). For the same reason `yamine proxy stop`
104
+ no longer stops `yamine start` apps along with the socket ones the
105
+ daemon booted: those have a supervising process of their own.
106
+
107
+ ### Fixed
108
+
109
+ - **A slow app and a dead app no longer come back as the same useless
110
+ 502.** Waiting for the backend's response *head* and waiting for body
111
+ bytes shared one clock, and `read_head` swallowed its own timeout into
112
+ the same `[nil, ...]` a dead backend returns — so `pipe_request` could
113
+ not tell them apart and every one of them got the same 60-byte page,
114
+ "The target app is not responding." A font request that took 60,068ms
115
+ came back with no hostname, no target, no owner, no directory and no
116
+ next step. The head now has its own budget
117
+ (`YAMINE_PROXY_HEAD_TIMEOUT`, default 60s — the same bound it already
118
+ got, so nothing that works today starts failing), the body keeps
119
+ `YAMINE_PROXY_IDLE_TIMEOUT` untouched, and a silent backend is reported
120
+ as what it is: *the backend at 127.0.0.1:3000 accepted the connection,
121
+ then sent no response for 60 seconds*, with the owning agent, the app's
122
+ own directory (`spec.dir`, not the proxy's cwd — inside a worktree that
123
+ is the wrong checkout), the path to its `log/development.log`, and the
124
+ `cd <dir> && yamine start` that fixes it. Machines get the same split
125
+ in a header: `x-yamine-error: backend-refused` or `backend-silent`.
126
+ A body clock that was tuned tight for one and a head clock that is not
127
+ are no longer the same dial, so a streaming response that goes quiet
128
+ between bytes still runs indefinitely.
129
+ - **A request that was merely slow now gets a second attempt.** One
130
+ retry, and only where a retry cannot duplicate work: a refused dial
131
+ never reached the app, so any method replays; a head timeout means the
132
+ head *did* arrive, so only bodiless GET, HEAD and OPTIONS replay. A
133
+ GET that takes 61 seconds to answer now serves instead of 502ing. A
134
+ POST never replays — the body is already consumed off the client socket
135
+ and cannot be resent faithfully, and a duplicated message is worse than
136
+ a slow page.
137
+ ## [0.21.2] — 2026-09-29
138
+
139
+ ### Fixed
140
+
141
+ - **Linux no longer orphans the app behind a boot's shell.** Every
142
+ process is spawned as `["sh", "-c", cmd]`, so the pid yamine tracks
143
+ is the *shell* — and on Linux a TERM to that shell does not reach the
144
+ app behind it, which reparents to init and keeps running. Every stop
145
+ path inherited the hole: `yamine stop` printed "Stopped <host>" and
146
+ left the app serving, Ctrl-C left the whole tree up, and
147
+ `yamine worktree remove` deleted the directory under a live process.
148
+ (macOS forwards the signal, which is why the unit suite never saw
149
+ it.) Each process now spawns as its own process-group leader and
150
+ stop signals the *group*, so one syscall reaches the shell and
151
+ everything below it. Only a pid that actually leads a group is
152
+ signalled by group id — the kernel is asked first — so the paths that
153
+ never had a shell in front of them (a directly-spawned puma, a pid
154
+ that is already gone) keep signalling exactly as before.
155
+ - **A stop that lands while the tree is still being built no longer
156
+ leaves a live process behind.** The group signal reaches the members
157
+ that exist at the instant it is sent, and a `sh -c` shell that has
158
+ not forked its app yet forks it *after* that sweep — so the app never
159
+ hears the TERM, reparents to init, and goes on serving with its
160
+ directory already on the way out. Measured on linux: TERM the group a
161
+ few milliseconds after the spawn and the shell dies while the app
162
+ comes up behind it and stays up. Asking was never the same as
163
+ stopping, and every caller that goes on to report "Stopped", drop the
164
+ route, or delete the worktree was exposed. Stops now ask and then
165
+ insist: TERM the group, let it leave on its own terms briefly, and
166
+ KILL whatever is still in it — liveness asked of the *group*, since a
167
+ dead leader that is a zombie still answers to its own pid while a live
168
+ tree behind it does not. The signal-trap path keeps the single-shot
169
+ signal, because a trap handler must not sleep.
170
+
5
171
  ## [0.21.1] — 2026-09-29
6
172
 
7
173
  ### 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: