yamine 0.13.1 → 0.13.2
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 +29 -0
- data/README.md +3 -1
- data/lib/ask/skills/yamine/SKILL.md +4 -2
- data/lib/yamine/cli/boot.rb +1 -1
- data/lib/yamine/cli/context.rb +15 -4
- data/lib/yamine/doctor.rb +16 -4
- data/lib/yamine/hosts.rb +56 -21
- data/lib/yamine/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 122503332845c1bbee6034e7262b763d38c2d4c849723e9519dfc7e91f0739b6
|
|
4
|
+
data.tar.gz: 5ae28759ce2ba01f7c9239056ca49d0ceda6f0958e26a534561fdcab76a485f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35f7dc98e1beaa8fcbf46dc2dbb735656789c193d2ed713e395fc038fa9a7801fcec6ea552c3affc67c82f0a3101b3200eb6126459f0588b723245318e257981
|
|
7
|
+
data.tar.gz: c3f54ddcc3cb3d0f6b99bca81770fc4addd61cadb22baf176c81042b9609edb079b69f2f41dd265378b08c44eb1a322dc00cffca580496741086750671950007
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.2] — 2026-09-12
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **"Resolves" now means "resolves for every client", not "the system
|
|
8
|
+
resolver said yes".** `Hosts.resolves?` asked `Addrinfo.getaddrinfo`,
|
|
9
|
+
which on macOS answers any `.localhost` name unconditionally — so
|
|
10
|
+
doctor and boot reported every `.localhost` route as fully resolved
|
|
11
|
+
even with no `/etc/hosts` entry, while a client that resolves only
|
|
12
|
+
from the hosts file (a CGO-disabled Go binary; AnyGit is the case
|
|
13
|
+
that found it) could not resolve any of them. That invisible gap is
|
|
14
|
+
why `sudo yamine hosts sync` existed as a manual step at all.
|
|
15
|
+
|
|
16
|
+
The check is now state-based. `Hosts.hosts_entry?` reads the hosts
|
|
17
|
+
file directly — any loopback mapping counts, hand-added entries
|
|
18
|
+
included, not just yamine's managed block — and `Hosts.resolution`
|
|
19
|
+
classifies each route as `ok` (every client resolves it), `warn`
|
|
20
|
+
(browsers and curl fine via the RFC 6761 `.localhost` special case;
|
|
21
|
+
file-only resolvers cannot see it), or `fail` (nothing resolves it).
|
|
22
|
+
Doctor renders the warn class with its existing `[warn]` state instead
|
|
23
|
+
of a failure — the 0.7.0 lesson, kept by severity and wording rather
|
|
24
|
+
than by a probe — and boot names the two gaps separately.
|
|
25
|
+
|
|
26
|
+
`.localhost` names are classified on file absence alone: the system
|
|
27
|
+
resolver's unconditional yes carries no information, and acting on it
|
|
28
|
+
was the bug. getaddrinfo now runs only for custom-TLD names the file
|
|
29
|
+
does not answer (real DNS or `/etc/resolver` setups stay `ok`), so
|
|
30
|
+
the common doctor path does no probing at all.
|
|
31
|
+
|
|
3
32
|
## [0.13.1] — 2026-09-12
|
|
4
33
|
|
|
5
34
|
### Fixed
|
data/README.md
CHANGED
|
@@ -122,7 +122,9 @@ destination pattern). Naming a variant also prefixes the hostname — see
|
|
|
122
122
|
below for how that differs from a worktree's automatic prefix.
|
|
123
123
|
|
|
124
124
|
`.localhost` resolves to loopback natively in Chrome, Firefox, and Edge —
|
|
125
|
-
no DNS server, no `/etc/resolver`. Safari
|
|
125
|
+
no DNS server, no `/etc/resolver`. Safari, custom TLDs, and resolvers
|
|
126
|
+
that read only `/etc/hosts` (CGO-disabled Go binaries are the common
|
|
127
|
+
case) need `yamine hosts sync`.
|
|
126
128
|
|
|
127
129
|
## Hostname shape
|
|
128
130
|
|
|
@@ -144,8 +144,10 @@ yamine prune # clear stale routes from crashed sessions
|
|
|
144
144
|
yamine start --json # boot readiness payload (pass/fail + log tail)
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
If a hostname does not resolve
|
|
148
|
-
|
|
147
|
+
If a hostname does not resolve — or boot/doctor report it as not in
|
|
148
|
+
`/etc/hosts` and you use clients that read only that file (CGO-disabled
|
|
149
|
+
Go binaries): `yamine hosts sync`. If the browser warns about TLS:
|
|
150
|
+
`yamine trust`.
|
|
149
151
|
|
|
150
152
|
## When NOT to use yamine
|
|
151
153
|
|
data/lib/yamine/cli/boot.rb
CHANGED
|
@@ -157,7 +157,7 @@ module Yamine
|
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
say opts
|
|
160
|
-
ctx.
|
|
160
|
+
ctx.report_resolution_gaps(routes_registered.flat_map { |r| r[:hostnames] })
|
|
161
161
|
|
|
162
162
|
# Supervisor: exit when ANY child dies (loud cleanup). pid => name
|
|
163
163
|
# so the message names the casualty instead of "a process".
|
data/lib/yamine/cli/context.rb
CHANGED
|
@@ -35,11 +35,22 @@ module Yamine
|
|
|
35
35
|
ProxyControl.proxy_tls(store)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
# Boot-time DNS report. Two distinct gaps, named separately: a fail
|
|
39
|
+
# resolves nowhere; a warn resolves in browsers and curl (the RFC
|
|
40
|
+
# 6761 .localhost special case) but not for clients that read only
|
|
41
|
+
# /etc/hosts — which is exactly the gap `hosts sync` closes.
|
|
42
|
+
def report_resolution_gaps(hostnames)
|
|
43
|
+
groups = Hosts.resolution(hostnames)
|
|
44
|
+
return if groups[:fail].empty? && groups[:warn].empty?
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
unless groups[:fail].empty?
|
|
47
|
+
warn "Warning: #{groups[:fail].join(", ")} will not resolve. Run: yamine hosts sync"
|
|
48
|
+
end
|
|
49
|
+
unless groups[:warn].empty?
|
|
50
|
+
warn "Warning: #{groups[:warn].join(", ")} not in /etc/hosts — browsers and curl " \
|
|
51
|
+
"resolve them anyway, but clients that only read /etc/hosts (CGO-disabled Go " \
|
|
52
|
+
"binaries) cannot. Run: yamine hosts sync if you use such clients."
|
|
53
|
+
end
|
|
43
54
|
end
|
|
44
55
|
|
|
45
56
|
def wait_for_exit(pid, timeout:)
|
data/lib/yamine/doctor.rb
CHANGED
|
@@ -160,12 +160,24 @@ module Yamine
|
|
|
160
160
|
hostnames = store.load_routes.map { |r| r["hostname"] }
|
|
161
161
|
return Check.new(name: "dns", ok: true, message: "no routes to resolve") if hostnames.empty?
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
groups = Hosts.resolution(hostnames)
|
|
164
|
+
problems = []
|
|
165
|
+
problems << "#{groups[:fail].join(", ")} do not resolve — run: yamine hosts sync" unless groups[:fail].empty?
|
|
166
|
+
unless groups[:warn].empty?
|
|
167
|
+
problems << "#{groups[:warn].join(", ")} not in /etc/hosts — browsers and curl " \
|
|
168
|
+
"resolve them anyway, but clients that only read the hosts file " \
|
|
169
|
+
"(CGO-disabled Go binaries) cannot — run: yamine hosts sync if you use such clients"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if problems.empty?
|
|
165
173
|
Check.new(name: "dns", ok: true, message: "all #{hostnames.length} hostname(s) resolve")
|
|
174
|
+
elsif groups[:fail].empty?
|
|
175
|
+
# Only the file-only-resolver gap: most clients are fine, so this
|
|
176
|
+
# is a warn, not a failure — failing here sent browser-only users
|
|
177
|
+
# to an elevated hosts write they did not need (the 0.7.0 lesson).
|
|
178
|
+
Check.new(name: "dns", ok: true, warn: true, message: problems.join("; "))
|
|
166
179
|
else
|
|
167
|
-
Check.new(name: "dns", ok: false,
|
|
168
|
-
message: "#{missing.join(", ")} do not resolve — run: yamine hosts sync")
|
|
180
|
+
Check.new(name: "dns", ok: false, message: problems.join("; "))
|
|
169
181
|
end
|
|
170
182
|
end
|
|
171
183
|
|
data/lib/yamine/hosts.rb
CHANGED
|
@@ -4,9 +4,11 @@ require "socket"
|
|
|
4
4
|
require "timeout"
|
|
5
5
|
|
|
6
6
|
module Yamine
|
|
7
|
-
# /etc/hosts sync for
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# /etc/hosts sync for the client classes that cannot resolve the routes
|
|
8
|
+
# on their own: custom TLDs, and resolvers that read only the hosts file
|
|
9
|
+
# (a CGO-disabled Go binary is the common case — no system resolver, no
|
|
10
|
+
# RFC 6761 special-casing). Same managed-block approach as portless
|
|
11
|
+
# hosts.ts.
|
|
10
12
|
module Hosts
|
|
11
13
|
BEGIN_MARKER = "# --- yamine begin ---"
|
|
12
14
|
END_MARKER = "# --- yamine end ---"
|
|
@@ -44,14 +46,13 @@ module Yamine
|
|
|
44
46
|
# Bring the managed block to exactly these hostnames. Idempotent: when
|
|
45
47
|
# the block already matches, this is a no-op returning true.
|
|
46
48
|
#
|
|
47
|
-
# The short-circuit is load-bearing, not an optimization. /etc/hosts is
|
|
49
|
+
# The sync short-circuit is load-bearing, not an optimization. /etc/hosts is
|
|
48
50
|
# root-owned on a normal machine, so a rewrite fails without sudo — and
|
|
49
51
|
# `setup` and every boot's workstation check run this. Without the
|
|
50
52
|
# guard, a machine whose hosts file was already correct got a
|
|
51
53
|
# "could not write /etc/hosts (try sudo yamine hosts sync)" warning on
|
|
52
54
|
# every run, sending people to an elevated write for a file that
|
|
53
|
-
# needed nothing.
|
|
54
|
-
# /etc/hosts only matters for Safari and custom TLDs.)
|
|
55
|
+
# needed nothing.
|
|
55
56
|
def sync(hostnames, path = PATH)
|
|
56
57
|
return true if synced?(hostnames, path)
|
|
57
58
|
|
|
@@ -85,26 +86,60 @@ module Yamine
|
|
|
85
86
|
false
|
|
86
87
|
end
|
|
87
88
|
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
#
|
|
92
|
-
#
|
|
89
|
+
# Loopback mappings that count as "this name reaches the proxy".
|
|
90
|
+
LOOPBACK = %w[127.0.0.1 ::1].freeze
|
|
91
|
+
|
|
92
|
+
# Does the hosts file map this hostname to loopback? That is the one
|
|
93
|
+
# resolution source every client shares — browsers and curl, the
|
|
94
|
+
# system resolver, and resolvers that read only the file (a
|
|
95
|
+
# CGO-disabled Go binary is the common case). The whole file is
|
|
96
|
+
# searched, not just the managed block, so a hand-added entry counts;
|
|
97
|
+
# only loopback mappings count, since the proxy serves on loopback
|
|
98
|
+
# and any other address points the name away from yamine.
|
|
99
|
+
def hosts_entry?(hostname, path = PATH)
|
|
100
|
+
target = hostname.to_s.downcase
|
|
101
|
+
File.foreach(path) do |line|
|
|
102
|
+
parts = line.split("#", 2).first.to_s.split
|
|
103
|
+
next unless parts.length > 1 && LOOPBACK.include?(parts.first.downcase)
|
|
104
|
+
return true if parts[1..].any? { |name| name.downcase == target }
|
|
105
|
+
end
|
|
106
|
+
false
|
|
107
|
+
rescue SystemCallError
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Where one hostname stands for the clients that will actually use
|
|
112
|
+
# it — a question with three answers no single resolver probe can
|
|
113
|
+
# produce:
|
|
114
|
+
#
|
|
115
|
+
# :ok — every client resolves it: mapped in the hosts file, or
|
|
116
|
+
# found by the system resolver without it (real DNS or an
|
|
117
|
+
# /etc/resolver entry for a custom TLD).
|
|
118
|
+
# :warn — browsers and curl handle it (the RFC 6761 .localhost
|
|
119
|
+
# special case), but file-only resolvers cannot see it.
|
|
120
|
+
# :fail — nothing resolves it.
|
|
93
121
|
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
def
|
|
122
|
+
# .localhost names are classified :warn on file absence alone. The
|
|
123
|
+
# system resolver answers any .localhost unconditionally, so probing
|
|
124
|
+
# it carries no information — and acting on that probe is how doctor
|
|
125
|
+
# and boot once reported every .localhost as fully resolved while a
|
|
126
|
+
# CGO-disabled Go binary could not resolve any of them.
|
|
127
|
+
def classification(hostname, path = PATH)
|
|
128
|
+
return :ok if hosts_entry?(hostname, path)
|
|
129
|
+
return :warn if hostname.to_s.downcase.end_with?(".localhost")
|
|
130
|
+
|
|
100
131
|
Timeout.timeout(2) { Addrinfo.getaddrinfo(hostname, nil) }
|
|
101
|
-
|
|
132
|
+
:ok
|
|
102
133
|
rescue SocketError, SystemCallError, Timeout::Error
|
|
103
|
-
|
|
134
|
+
:fail
|
|
104
135
|
end
|
|
105
136
|
|
|
106
|
-
|
|
107
|
-
|
|
137
|
+
# Partition hostnames into { ok:, warn:, fail: } for the consumers
|
|
138
|
+
# that report one line per state (doctor, boot).
|
|
139
|
+
def resolution(hostnames, path = PATH)
|
|
140
|
+
groups = { ok: [], warn: [], fail: [] }
|
|
141
|
+
hostnames.each { |h| groups[classification(h, path)] << h }
|
|
142
|
+
groups
|
|
108
143
|
end
|
|
109
144
|
end
|
|
110
145
|
end
|
data/lib/yamine/version.rb
CHANGED