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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43ea000326072db8f0173360b3ef58a6cce132e20f372b65f0e777bafbb97e9d
4
- data.tar.gz: 3e878d2cae8afffd1dca255a006a5863e12dbd4471a4b6214656dda5bc97d37f
3
+ metadata.gz: 122503332845c1bbee6034e7262b763d38c2d4c849723e9519dfc7e91f0739b6
4
+ data.tar.gz: 5ae28759ce2ba01f7c9239056ca49d0ceda6f0958e26a534561fdcab76a485f3
5
5
  SHA512:
6
- metadata.gz: 272dae53dfac6b0521c09c00ae096cdc10620dcabf8064a286a0fe25813a61738b3fefc4bb34cf8fdfdd8dd1467a0064ec4e8ceb6ec7b262a8ed638b66101bda
7
- data.tar.gz: e16540d1830850c50a54451ec5f0bfb0ad87fd770b111a0b4276aad9a6916dadeeed4c106b61392e5fc5794e6276f003f718a5e53135885f36cc142944d1a12f
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 may need `yamine hosts sync`.
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: `yamine hosts sync`. If the browser
148
- warns about TLS: `yamine trust`.
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
 
@@ -157,7 +157,7 @@ module Yamine
157
157
  end
158
158
 
159
159
  say opts
160
- ctx.report_unresolved(routes_registered.flat_map { |r| r[:hostnames] })
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".
@@ -35,11 +35,22 @@ module Yamine
35
35
  ProxyControl.proxy_tls(store)
36
36
  end
37
37
 
38
- def report_unresolved(hostnames)
39
- missing = Hosts.unresolved(hostnames)
40
- return if missing.empty?
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
- warn "Warning: #{missing.join(", ")} will not resolve. Run: yamine hosts sync"
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
- missing = Hosts.unresolved(hostnames)
164
- if missing.empty?
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 Safari + custom TLDs (.localhost resolves natively
8
- # in Chrome/Firefox/Edge; Safari uses the system resolver).
9
- # Same managed-block approach as portless hosts.ts.
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. (Chrome/Firefox/Edge resolve *.localhost natively;
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
- # Does the hostname resolve for the programs that will actually use
89
- # it browsers, curl, the app's own HTTP clients? That question can
90
- # only be answered by the SYSTEM resolver (getaddrinfo), which reads
91
- # nsswitch (so /etc/hosts counts) and implements the RFC 6761
92
- # special-use TLDs.
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
- # Ruby's Resolv is a pure-Ruby DNS client: it never sees /etc/hosts
95
- # and knows nothing about RFC 6761, so it reports "no address for
96
- # myapp.localhost" on a machine where .localhost resolves perfectly.
97
- # That false negative sent users to `sudo yamine hosts sync` — an
98
- # elevated write to /etc/hosts to fix a problem they did not have.
99
- def resolves?(hostname)
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
- true
132
+ :ok
102
133
  rescue SocketError, SystemCallError, Timeout::Error
103
- false
134
+ :fail
104
135
  end
105
136
 
106
- def unresolved(hostnames)
107
- hostnames.reject { |h| resolves?(h) }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yamine
4
- VERSION = "0.13.1"
4
+ VERSION = "0.13.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yamine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.1
4
+ version: 0.13.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto