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 +4 -4
- data/CHANGELOG.md +166 -0
- data/README.md +39 -12
- data/lib/ask/skills/yamine/SKILL.md +20 -1
- data/lib/yamine/cli/boot.rb +311 -30
- data/lib/yamine/cli/context.rb +1 -1
- data/lib/yamine/cli/routes.rb +106 -22
- data/lib/yamine/cli/system.rb +13 -3
- data/lib/yamine/cli/worktree.rb +14 -6
- data/lib/yamine/cli.rb +3 -2
- data/lib/yamine/process_tree.rb +256 -0
- data/lib/yamine/proxy.rb +215 -46
- data/lib/yamine/runner.rb +29 -9
- data/lib/yamine/supervisor.rb +147 -15
- data/lib/yamine/version.rb +1 -1
- data/lib/yamine.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69848a3d38cd9997ca18e52ea892aa57962524e0cd41df16199a651409e86b76
|
|
4
|
+
data.tar.gz: aebe3dd18c4bab95865491850bb940ca9a7e42c83f53d851b4d3ddd691f80305
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 (+
|
|
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 (
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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:
|