yamine 0.15.2 → 0.17.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.
@@ -32,6 +32,17 @@ module Yamine
32
32
 
33
33
  MAX_IDENTIFIER_BYTES = 63
34
34
  STATE_FILE = "databases.json"
35
+ # The per-worktree environment files yamine writes: `.env` carries
36
+ # the development set for ANY dotenv-style loader (framework-agnostic),
37
+ # `.env.test` carries PRIMARY_DATABASE_URL — the key Rails checks
38
+ # *before* DATABASE_URL for a flat test config, so the test URL wins
39
+ # no matter which order a loader reads the two files in. Hand-run
40
+ # commands (`rails console`, `rails test`, `db:migrate`) pick these
41
+ # up; env injection still covers processes yamine spawns itself.
42
+ ENV_FILES = %w[.env .env.development .env.test].freeze
43
+ # 0.16.0's marker file — removed in favor of .env; deleted on sight
44
+ # so upgraded worktrees don't accumulate dead files.
45
+ LEGACY_MARKER_FILE = ".yamine-db-suffix"
35
46
 
36
47
  # Database name for a worktree dir + env (development/test).
37
48
  # Main checkout (no worktree marker) keeps the bare name.
@@ -77,7 +88,12 @@ module Yamine
77
88
  if map.key?(name) && map[name]["dir"] != dir
78
89
  return nil
79
90
  end
80
- map[name] = { "dir" => dir, "claimed_at" => Time.now.utc.iso8601 }
91
+ # Merge, don't replace: boot re-claims on every run, and a
92
+ # multi-database claim's suffix/names/bases are stored under
93
+ # this key — a fresh hash would erase them and orphan the whole
94
+ # set on the next drop.
95
+ map[name] = (map[name] || {}).merge("dir" => dir,
96
+ "claimed_at" => Time.now.utc.iso8601)
81
97
  save_map(state_dir, map)
82
98
  name
83
99
  rescue SystemCallError
@@ -93,13 +109,134 @@ module Yamine
93
109
  {}
94
110
  end
95
111
 
112
+ # Claim URLs carry database passwords (the app's own resolved
113
+ # config, probed at worktree-add time), so the state file is
114
+ # private — it held no secrets before multi-database claims.
96
115
  def save_map(state_dir, map)
97
116
  FileUtils.mkdir_p(state_dir)
98
- File.write(File.join(state_dir, STATE_FILE), JSON.pretty_generate(map))
117
+ path = File.join(state_dir, STATE_FILE)
118
+ File.write(path, JSON.pretty_generate(map))
119
+ File.chmod(0o600, path)
99
120
  rescue SystemCallError
100
121
  nil
101
122
  end
102
123
 
124
+ # ── multi-database claims ─────────────────────────────────────
125
+ #
126
+ # A multi-database claim carries the whole set beside the usual
127
+ # dir/claimed_at:
128
+ # suffix the token every database name gains — also the marker
129
+ # file's contents, the one string yamine and the app's
130
+ # database.yml must agree on, byte for byte
131
+ # names config name -> actual database name
132
+ # bases config name -> the app's own URL (server coordinates
133
+ # for create/drop; where the suffix gets inserted)
134
+ # Single-database claims predate this and have none of the three;
135
+ # every reader falls back to the claim key as the one name.
136
+
137
+ # The suffix for a worktree: the claim key minus the env. Base
138
+ # names already carry the environment (myrr_markdown_development),
139
+ # so appending "_development" a second time only burns identifier
140
+ # budget. The claim key keeps the env (and the collision guard);
141
+ # the suffix is what has to fit inside a database name.
142
+ def suffix_for(claim_name, env)
143
+ stripped = claim_name.delete_suffix("_#{env}")
144
+ stripped.empty? ? claim_name : stripped
145
+ end
146
+
147
+ # Fit the suffix so `base + "_" + suffix` stays within the
148
+ # identifier cap for ANY base: budget against the longest one.
149
+ # Fitting happens once, here, because the marker file holds the
150
+ # suffix and the app only concatenates — a hand-run `rails
151
+ # console` must arrive at byte-identical names with no truncation
152
+ # logic of its own. Returns nil when even a hash-shortened suffix
153
+ # cannot fit (a base name so long no suffix follows it); callers
154
+ # name the problem instead of guessing.
155
+ def fit_suffix(suffix, dir, base_databases)
156
+ longest = base_databases.map { |d| d.to_s.bytesize }.max || 0
157
+ budget = MAX_IDENTIFIER_BYTES - longest - 1
158
+ return suffix if budget.positive? && suffix.bytesize <= budget
159
+ return nil if budget < 8
160
+
161
+ keep = budget - 7
162
+ "#{suffix.byteslice(0, keep).gsub(/_+\z/, "")}_#{short_hash(dir)}"
163
+ end
164
+
165
+ # The actual database name for one base under a suffix. Raises
166
+ # rather than truncating: silent truncation would diverge from
167
+ # what the marker file produces inside the app, and yamine names
168
+ # and app names disagreeing is exactly the bug this prevents.
169
+ # fit_suffix makes this unreachable for fitted suffixes; the
170
+ # raise guards programmatic misuse.
171
+ def multiname(base_database, suffix)
172
+ name = "#{base_database}_#{suffix}"
173
+ raise ArgumentError,
174
+ "database name exceeds #{MAX_IDENTIFIER_BYTES} bytes: #{name}" if name.bytesize > MAX_IDENTIFIER_BYTES
175
+
176
+ name
177
+ end
178
+
179
+ # Write the worktree's environment files: `.env` with the whole
180
+ # development set (dotenv-compatible — any framework or plain Ruby
181
+ # app that loads .env gets hand-run isolation for free) and
182
+ # `.env.test` with the test URL under PRIMARY_DATABASE_URL.
183
+ # Private (URLs carry whatever the app's config carries), and the
184
+ # 0.16.0 marker file is removed on the way past.
185
+ def write_env_files(dir, dev_env, test_url: nil)
186
+ FileUtils.rm_f(File.join(dir, LEGACY_MARKER_FILE))
187
+ return if dev_env.nil? && test_url.nil?
188
+
189
+ unless dev_env.nil? || dev_env.empty?
190
+ body = dotenv_body(dev_env.reject { |k, _v| k == "PRIMARY_DATABASE_URL" })
191
+ write_env_file(File.join(dir, ".env"), body)
192
+ # Same content under the env-specific name: a loader that
193
+ # prefers .env.development over .env finds identical values.
194
+ write_env_file(File.join(dir, ".env.development"), body)
195
+ end
196
+ return if test_url.nil? || test_url.to_s.empty?
197
+
198
+ write_env_file(File.join(dir, ".env.test"),
199
+ dotenv_body({ "PRIMARY_DATABASE_URL" => test_url }))
200
+ end
201
+
202
+ def write_env_file(path, body)
203
+ File.write(path, body)
204
+ File.chmod(0o600, path)
205
+ end
206
+
207
+ # dotenv format: single-quoted values (literal — no interpolation
208
+ # of a password's `$` or `#`), with the two escapes the dotenv
209
+ # grammar allows inside them.
210
+ def dotenv_body(env)
211
+ header = "# Per-worktree databases, written by yamine (worktree add).\n" \
212
+ "# Loaded by dotenv-rails / any dotenv loader; removed with this worktree.\n"
213
+ lines = env.map { |k, v| "#{k}='#{dotenv_escape(v.to_s)}'" }
214
+ "#{header}#{lines.join("\n")}\n"
215
+ end
216
+
217
+ def dotenv_escape(value)
218
+ value.gsub("\\") { "\\\\" }.gsub("'") { "\\'" }
219
+ end
220
+
221
+ # Keep files out of `git status` without touching the committed
222
+ # .gitignore: git's exclude file lives in the shared git dir, so
223
+ # entries cover every worktree of the repo. Idempotent per line.
224
+ def exclude_files(dir, names)
225
+ out, status = Open3.capture2("git", "-C", dir, "rev-parse", "--git-path", "info/exclude")
226
+ return unless status.success?
227
+
228
+ path = out.strip
229
+ path = File.expand_path(path, dir) unless path.start_with?("/")
230
+ FileUtils.mkdir_p(File.dirname(path))
231
+ existing = File.file?(path) ? File.read(path).lines.map(&:strip) : []
232
+ additions = Array(names).reject { |n| existing.include?(n) }
233
+ return if additions.empty?
234
+
235
+ File.open(path, "a") { |f| additions.each { |n| f.puts n } }
236
+ rescue SystemCallError, IOError
237
+ nil
238
+ end
239
+
103
240
  # The DATABASE_URL template per-worktree names derive from: ENV
104
241
  # first, then the config's top-level env.clear, then per-process
105
242
  # env.clear (configs written before top-level env existed). Shared
@@ -127,6 +264,29 @@ module Yamine
127
264
  load_map(state_dir).select { |_, v| !File.directory?(v["dir"]) }
128
265
  end
129
266
 
267
+ # The multi-database claim under `key`, or nil — single-database
268
+ # claims (and foreign keys) simply have no names, and every reader
269
+ # falls back to the key itself as the one database name.
270
+ def multi_claim(state_dir, key)
271
+ info = load_map(state_dir)[key]
272
+ info if info.is_a?(Hash) && info["names"].is_a?(Hash) && !info["names"].empty?
273
+ end
274
+
275
+ # Every [database_name, server_url] pair a claim stands for: N
276
+ # pairs for a multi claim, nil for a legacy single claim (caller
277
+ # falls back to the key + its own template lookup). bases may lag
278
+ # names on older claims; a missing base falls back to the first —
279
+ # all of an app's databases live on one server in every layout
280
+ # yamine has seen.
281
+ def claim_pairs(info)
282
+ names = info.is_a?(Hash) ? info["names"] : nil
283
+ return nil if names.nil? || names.empty?
284
+
285
+ bases = info["bases"] || {}
286
+ default = bases.values.first
287
+ names.filter_map { |cfg, db| [db, (bases[cfg] || default)] }
288
+ end
289
+
130
290
  # Adapter detection from DATABASE_URL scheme. Returns :postgres,
131
291
  # :mysql, :sqlite, or :unknown. Only postgres/mysql get create +
132
292
  # DATABASE_URL treatment; sqlite is left alone (relative paths
@@ -189,7 +349,11 @@ module Yamine
189
349
  return :failed unless reachable
190
350
 
191
351
  uri = URI.parse(database_url)
192
- _out, status = Open3.capture2("dropdb", name, env: pg_env(uri))
352
+ # env as the FIRST positional (like ensure_postgres): Open3
353
+ # forwards kwargs to spawn, and spawn has no env: option — as a
354
+ # kwarg this raised ArgumentError on every real drop, crashing
355
+ # teardown after the routes were already stopped.
356
+ _out, status = Open3.capture2(pg_env(uri), "dropdb", name)
193
357
  return :dropped if status.success?
194
358
 
195
359
  reachable, exists = pg_state(name, database_url)
@@ -293,9 +457,12 @@ module Yamine
293
457
  args
294
458
  end
295
459
 
460
+ # PGHOST only when the URL names a host: an empty authority
461
+ # (postgres:///dbname) means the unix socket, and forcing
462
+ # 127.0.0.1 there would reach the wrong server — or none.
296
463
  def pg_env(uri)
297
464
  {
298
- "PGHOST" => uri.host || "127.0.0.1",
465
+ "PGHOST" => (uri.host unless uri.host.to_s.empty?),
299
466
  "PGPORT" => (uri.port || 5432).to_s,
300
467
  "PGUSER" => URI.decode_www_form_component(uri.user || ENV["USER"].to_s),
301
468
  "PGPASSWORD" => uri.password ? URI.decode_www_form_component(uri.password) : nil
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+ require "timeout"
6
+
7
+ module Yamine
8
+ # What databases an app actually has — asked, never guessed.
9
+ #
10
+ # A Rails multi-database app keeps its per-database URLs wherever it
11
+ # likes (credentials, ENV, hard-coded); yamine must not parse
12
+ # database.yml or touch decryption keys to learn them. The app
13
+ # itself answers: `bin/rails runner` prints the resolved
14
+ # configuration for the environment, so whatever the app would
15
+ # really connect to is exactly what yamine provisions, names, and
16
+ # drops. Credentials decrypt inside that process — the probe never
17
+ # sees a key.
18
+ #
19
+ # The runner emits one `YAMINE_DBS=<json>` line; Rails boot noise
20
+ # shares stdout, so the marker (never the last line) finds the
21
+ # payload. Any failure — no bin/rails, boot error, timeout — returns
22
+ # nil: the caller falls back to the single-database path. A probe is
23
+ # an enhancement, not a requirement, and it must never take a boot
24
+ # down with it.
25
+ module Probe
26
+ module_function
27
+
28
+ MARKER = "YAMINE_DBS="
29
+ TIMEOUT = 90
30
+
31
+ # Runs inside the app (bin/rails runner), so configuration_hash is
32
+ # fully resolved: Rails has already parsed any `url:` into
33
+ # components and decrypted credentials. The URL is rebuilt here
34
+ # from those components — a password travels from credentials to
35
+ # the claim file without yamine ever parsing config itself.
36
+ # SQLite-family adapters keep their scheme and skip the host part;
37
+ # relative paths already isolate per-worktree, so they never enter
38
+ # the multi-database claim.
39
+ SCRIPT = <<~'RUBY'
40
+ require "json"
41
+ require "uri"
42
+ rows = ActiveRecord::Base.configurations
43
+ .configs_for(env_name: ENV.fetch("RAILS_ENV", "development"))
44
+ .map do |c|
45
+ h = c.configuration_hash
46
+ db = h[:database].to_s
47
+ url = h[:url].to_s
48
+ if url.empty?
49
+ scheme = { "postgresql" => "postgres", "postgres" => "postgres",
50
+ "postgis" => "postgres", "mysql2" => "mysql",
51
+ "trilogy" => "mysql" }[h[:adapter].to_s]
52
+ if scheme
53
+ info = +""
54
+ if h[:username]
55
+ info << URI.encode_www_form_component(h[:username].to_s)
56
+ if h[:password]
57
+ info << ":" << URI.encode_www_form_component(h[:password].to_s)
58
+ end
59
+ info << "@"
60
+ end
61
+ host = h[:host].to_s
62
+ authority = host.empty? ? "#{info}" : "#{info}#{host}#{h[:port] ? ":#{h[:port]}" : ""}"
63
+ url = "#{scheme}://#{authority}/#{db}"
64
+ else
65
+ url = "#{h[:adapter]}:#{db}"
66
+ end
67
+ end
68
+ { "name" => c.name, "database" => db, "url" => url,
69
+ # What DATABASE_URL held INSIDE the app — the discriminator
70
+ # between "no .env loader ran" (nil) and "loader ran but
71
+ # database.yml's url: keys took precedence over it".
72
+ "env_database_url" => ENV["DATABASE_URL"] }
73
+ end
74
+ puts "YAMINE_DBS=#{JSON.generate(rows)}"
75
+ RUBY
76
+
77
+ # [{ "name", "database", "url" }, ...] for the environment, or nil
78
+ # when the app cannot answer (not Rails, boot failure, timeout).
79
+ def rails_databases(dir, env: nil)
80
+ env ||= ENV.fetch("RAILS_ENV", "development")
81
+ bin = File.join(dir, "bin", "rails")
82
+ return nil unless File.file?(bin)
83
+
84
+ out = +""
85
+ status = nil
86
+ # A stray DATABASE_URL (or NAME_DATABASE_URL) in the caller's
87
+ # environment would win over database.yml inside the resolver —
88
+ # the probe must see the app's own configuration. spawn MERGES
89
+ # the env hash over the inherited one, so merely omitting the
90
+ # keys would leave them in place: pass them as nil to unset.
91
+ scrub = ENV.keys
92
+ .select { |k| k == "DATABASE_URL" || k.end_with?("_DATABASE_URL") }
93
+ .to_h { |k| [k, nil] }
94
+ child_env = scrub.merge("RAILS_ENV" => env)
95
+ Open3.popen2e(child_env, bin, "runner", SCRIPT, chdir: dir) do |stdin, stdout, wait|
96
+ stdin.close
97
+ begin
98
+ Timeout.timeout(TIMEOUT) { out = stdout.read }
99
+ rescue Timeout::Error
100
+ Process.kill("KILL", wait.pid) rescue nil
101
+ wait.value rescue nil
102
+ return nil
103
+ end
104
+ status = wait.value
105
+ end
106
+ return nil unless status&.success?
107
+
108
+ parse(out)
109
+ rescue SystemCallError, IOError
110
+ nil
111
+ end
112
+
113
+ # The marker line amidst whatever the boot printed. JSON parse
114
+ # failure returns nil rather than raising: garbage in, "cannot
115
+ # probe" out.
116
+ def parse(output)
117
+ line = output.to_s.lines.find { |l| l.start_with?(MARKER) }
118
+ return nil unless line
119
+
120
+ rows = JSON.parse(line.delete_prefix(MARKER).strip)
121
+ return nil unless rows.is_a?(Array)
122
+
123
+ rows.select { |r| r.is_a?(Hash) && r["name"] && r["url"] && r["database"] }
124
+ .then { |r| r.empty? ? nil : r }
125
+ rescue JSON::ParserError
126
+ nil
127
+ end
128
+
129
+ # The subset that lives on a server (postgres/mysql). SQLite and
130
+ # friends need no provisioning — their relative paths are already
131
+ # per-worktree — so they never enter a claim.
132
+ def server_backed(rows)
133
+ rows.select { |r| %i[postgres mysql].include?(Yamine::Database.adapter_for(r["url"])) }
134
+ end
135
+ end
136
+ end
data/lib/yamine/proxy.rb CHANGED
@@ -485,7 +485,12 @@ module Yamine
485
485
  items = routes.map { |r| "<li>#{escape(r["hostname"])}</li>" }.join
486
486
  body = "<h1>No app registered for #{escape(bare)}</h1>" \
487
487
  "#{hint}<ul>#{items}</ul>"
488
- respond(sock, 404, body)
488
+ # 503, not 404: 404 says the app answered and has nothing at that path,
489
+ # and a client that believes it goes looking for a bug in the app. This
490
+ # is the proxy saying the app is not there — the same answer a dead
491
+ # backend gets (502 Bad Gateway), so no caller has to read the body to
492
+ # tell "not running" from "the app said no".
493
+ respond(sock, 503, body)
489
494
  end
490
495
 
491
496
  def render_bad_gateway(sock)
@@ -68,7 +68,8 @@ module Yamine
68
68
  end
69
69
 
70
70
  # Pre-flight: verify runtime dependencies before spawning anything.
71
- # Ruby (bundle check), Node (node_modules present). Returns [ok, fix].
71
+ # Ruby (bundle check), Node (node_modules present — but only when the
72
+ # manifest actually declares dependencies). Returns [ok, fix].
72
73
  # Fast-fails with the fix instead of letting Puma crash-loop for 60s.
73
74
  def check_deps(dir = Dir.pwd)
74
75
  gemfile = File.join(dir, "Gemfile")
@@ -80,7 +81,8 @@ module Yamine
80
81
  end
81
82
  end
82
83
  package = File.join(dir, "package.json")
83
- if File.file?(package) && !File.directory?(File.join(dir, "node_modules"))
84
+ if File.file?(package) && !File.directory?(File.join(dir, "node_modules")) &&
85
+ package_declares_dependencies?(package)
84
86
  return [false, "node_modules missing — run `npm install` in #{dir}"]
85
87
  end
86
88
  [true, nil]
@@ -88,6 +90,29 @@ module Yamine
88
90
  [false, e.message.lines.first&.strip]
89
91
  end
90
92
 
93
+ # Does this manifest need an install at all? A stub `{}`
94
+ # (dependency-less apps keep one so tooling finds a manifest) has
95
+ # nothing to install — `npm install` creates no node_modules for
96
+ # it — and demanding the directory would fail every fresh clone
97
+ # and worktree of such an app forever. An unreadable manifest
98
+ # keeps the old directory check: honest conservatism.
99
+ def package_declares_dependencies?(path)
100
+ data = JSON.parse(File.read(path))
101
+ return false unless data.is_a?(Hash)
102
+
103
+ %w[dependencies devDependencies optionalDependencies peerDependencies workspaces]
104
+ .any? do |key|
105
+ value = data[key]
106
+ if value.is_a?(Hash) || value.is_a?(Array)
107
+ !value.empty?
108
+ else
109
+ !value.nil?
110
+ end
111
+ end
112
+ rescue JSON::ParserError, SystemCallError
113
+ true
114
+ end
115
+
91
116
  # Poll an HTTP route until healthy or timeout. Healthy = healthcheck
92
117
  # path returns 2xx-3xx, or (no healthcheck) TCP accept on the port.
93
118
  # Returns [healthy, detail].
data/lib/yamine/runner.rb CHANGED
@@ -100,11 +100,11 @@ module Yamine
100
100
  # yamine's own keys (PORT, YAMINE_URL, DATABASE_URL, the TLS vars)
101
101
  # win over it, because those describe the boot rather than the app.
102
102
  def boot_run(name:, hostname:, url:, dir:, command:, port: nil, force: false,
103
- rails_dev_host: nil, register: true, database_url: nil, spec: nil,
104
- extra_env: nil, subdomains: false)
103
+ rails_dev_host: nil, register: true, database_url: nil, database_env: nil,
104
+ spec: nil, extra_env: nil, subdomains: false)
105
105
  port ||= Ports.find_free
106
106
  env = child_env(dir, url: url, port: port, rails_dev_host: rails_dev_host,
107
- database_url: database_url, extra_env: extra_env)
107
+ database_url: database_url, database_env: database_env, extra_env: extra_env)
108
108
  path = log_path(dir, name)
109
109
  pid = with_clean_env { spawn(env, *command, chdir: dir, out: path, err: [:child, :out]) }
110
110
  Process.detach(pid)
@@ -131,9 +131,9 @@ module Yamine
131
131
  # Returns the placeholder App (target known before bind). Fate of
132
132
  # the backend is decided by wait, not by spawn.
133
133
  def spawn_http(name:, hostname:, url:, dir:, command:, port:, rails_dev_host: nil,
134
- database_url: nil, force: false, extra_env: nil)
134
+ database_url: nil, database_env: nil, force: false, extra_env: nil)
135
135
  env = child_env(dir, url: url, port: port, rails_dev_host: rails_dev_host,
136
- database_url: database_url, extra_env: extra_env)
136
+ database_url: database_url, database_env: database_env, extra_env: extra_env)
137
137
  path = log_path(dir, name)
138
138
  pid = with_clean_env { spawn(env, *command, chdir: dir, out: path, err: [:child, :out]) }
139
139
  Process.detach(pid)
@@ -158,7 +158,8 @@ module Yamine
158
158
  false
159
159
  end
160
160
 
161
- def child_env(dir, url:, port:, rails_dev_host: nil, database_url: nil, extra_env: nil)
161
+ def child_env(dir, url:, port:, rails_dev_host: nil, database_url: nil,
162
+ database_env: nil, extra_env: nil)
162
163
  # The config's own variables first, so everything below overrides
163
164
  # them: PORT, YAMINE_URL, DATABASE_URL, and the TLS paths describe
164
165
  # *this boot* and must not be lost to a stray key in local.yml.
@@ -190,10 +191,17 @@ module Yamine
190
191
  # proxied hostname so Rails apps boot behind yamine with zero
191
192
  # config — this replaces the yamine-rails hosts patch.
192
193
  env["RAILS_DEVELOPMENT_HOSTS"] = rails_dev_host if rails_dev_host
193
- # Per-worktree database: every process in this worktree shares one
194
- # isolated database. Frameworks that honor DATABASE_URL (Rails,
195
- # Django, most Node) get isolation for free; others ignore it.
196
- env["DATABASE_URL"] = database_url if database_url
194
+ # Per-worktree databases: the whole isolated set this checkout
195
+ # claims — DATABASE_URL for primary plus NAME_DATABASE_URL per
196
+ # extra config (Rails' own convention), so a multi-database app
197
+ # gets every database redirected, not just the first. Frameworks
198
+ # that honor DATABASE_URL (Rails, Django, most Node) get
199
+ # isolation for free; others ignore it.
200
+ if database_env
201
+ database_env.each { |k, v| env[k.to_s] = v.to_s if v }
202
+ elsif database_url
203
+ env["DATABASE_URL"] = database_url
204
+ end
197
205
  env
198
206
  end
199
207
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yamine
4
- VERSION = "0.15.2"
4
+ VERSION = "0.17.0"
5
5
  end
data/lib/yamine.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "yamine/version"
4
4
  require_relative "yamine/errors"
5
5
  require_relative "yamine/agent"
6
6
  require_relative "yamine/database"
7
+ require_relative "yamine/probe"
7
8
  require_relative "yamine/sanitize"
8
9
  require_relative "yamine/hostname"
9
10
  require_relative "yamine/inference"
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.15.2
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -103,6 +103,7 @@ files:
103
103
  - lib/yamine/log.rb
104
104
  - lib/yamine/ownership.rb
105
105
  - lib/yamine/ports.rb
106
+ - lib/yamine/probe.rb
106
107
  - lib/yamine/procfile.rb
107
108
  - lib/yamine/proxy.rb
108
109
  - lib/yamine/proxy_control.rb