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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +130 -0
- data/README.md +96 -6
- data/lib/ask/skills/yamine/SKILL.md +40 -13
- data/lib/yamine/cli/boot.rb +311 -42
- data/lib/yamine/cli/system.rb +105 -26
- data/lib/yamine/cli/worktree.rb +145 -45
- data/lib/yamine/cli.rb +1 -1
- data/lib/yamine/database.rb +171 -4
- data/lib/yamine/probe.rb +136 -0
- data/lib/yamine/proxy.rb +6 -1
- data/lib/yamine/readiness.rb +27 -2
- data/lib/yamine/runner.rb +18 -10
- data/lib/yamine/version.rb +1 -1
- data/lib/yamine.rb +1 -0
- metadata +2 -1
data/lib/yamine/cli/system.rb
CHANGED
|
@@ -861,18 +861,24 @@ module Yamine
|
|
|
861
861
|
puts " # Prefer Kamal multi-host for production; single-host preview above is fine for ephemeral branches."
|
|
862
862
|
end
|
|
863
863
|
|
|
864
|
-
# Per-worktree databases: list/create/drop
|
|
865
|
-
# `db list` shows every
|
|
866
|
-
# `db create`
|
|
867
|
-
#
|
|
868
|
-
#
|
|
864
|
+
# Per-worktree databases: list/create/drop/describe.
|
|
865
|
+
# `db list` shows every claim and every database under it.
|
|
866
|
+
# `db create` asks the app what it has and provisions it — the
|
|
867
|
+
# suffixed set in a worktree, the base set in the main checkout
|
|
868
|
+
# (self-healing: run it after adding a database to the app).
|
|
869
|
+
# `db drop <name>` drops a claim's whole set — by claim key or
|
|
870
|
+
# by any actual database name (refuses without --force when its
|
|
871
|
+
# worktree still exists).
|
|
872
|
+
# `db describe` asks the app what it has and prints it — the
|
|
873
|
+
# probe behind create, exposed for debugging.
|
|
869
874
|
def db(ctx, args)
|
|
870
875
|
sub = args.first
|
|
871
876
|
case sub
|
|
872
877
|
when "list", nil then db_list(ctx)
|
|
873
878
|
when "create" then db_create(ctx)
|
|
874
879
|
when "drop" then db_drop(ctx, args[1..] || [])
|
|
875
|
-
|
|
880
|
+
when "describe" then db_describe(ctx)
|
|
881
|
+
else raise Error, "Usage: yamine db [list|create|drop <name> [--force]|describe]"
|
|
876
882
|
end
|
|
877
883
|
end
|
|
878
884
|
|
|
@@ -880,32 +886,64 @@ module Yamine
|
|
|
880
886
|
map = Database.load_map(ctx.store.dir)
|
|
881
887
|
if map.empty?
|
|
882
888
|
puts "No worktree databases claimed yet."
|
|
883
|
-
puts "
|
|
889
|
+
puts "Run `yamine worktree add <branch>` — or `yamine db create` — to claim one."
|
|
884
890
|
return
|
|
885
891
|
end
|
|
886
892
|
map.each do |name, info|
|
|
887
893
|
alive = File.directory?(info["dir"]) ? "" : " (worktree gone)"
|
|
888
894
|
puts " #{name} -> #{info["dir"]}#{alive}"
|
|
895
|
+
names = info.is_a?(Hash) ? info["names"] : nil
|
|
896
|
+
next unless names.is_a?(Hash) && !names.empty?
|
|
897
|
+
|
|
898
|
+
names.each_value { |db| puts " db: #{db}" }
|
|
889
899
|
end
|
|
890
900
|
end
|
|
891
901
|
|
|
892
902
|
def db_create(ctx)
|
|
893
|
-
|
|
894
|
-
name = Database.name_for(Dir.pwd, env: boot_env_name,
|
|
903
|
+
claim_key = Database.name_for(Dir.pwd, env: boot_env_name,
|
|
895
904
|
state_dir: ctx.store.dir)
|
|
905
|
+
rows = Probe.rails_databases(Dir.pwd)
|
|
906
|
+
server = rows && Probe.server_backed(rows)
|
|
907
|
+
|
|
908
|
+
if server && !server.empty?
|
|
909
|
+
if Database.main_checkout?(Dir.pwd)
|
|
910
|
+
db_create_main(server)
|
|
911
|
+
else
|
|
912
|
+
names = BootCommand.provision_multidb(ctx, Dir.pwd, claim_key, server)
|
|
913
|
+
puts "Database set ready (#{names.size}):"
|
|
914
|
+
names.each_value { |db| puts " #{db}" }
|
|
915
|
+
end
|
|
916
|
+
return
|
|
917
|
+
end
|
|
918
|
+
|
|
896
919
|
template = drop_template
|
|
897
920
|
unless template && !template.strip.empty?
|
|
898
921
|
$stderr.puts "Error: no DATABASE_URL template found (ENV or env.clear in config/local.yml)."
|
|
899
922
|
exit 1
|
|
900
923
|
end
|
|
901
|
-
if Database.ensure_exists(
|
|
902
|
-
puts "Database #{
|
|
924
|
+
if Database.ensure_exists(claim_key, template)
|
|
925
|
+
puts "Database #{claim_key} ready."
|
|
903
926
|
else
|
|
904
|
-
$stderr.puts "Error: could not create #{
|
|
927
|
+
$stderr.puts "Error: could not create #{claim_key}. Check the database server is running."
|
|
905
928
|
exit 1
|
|
906
929
|
end
|
|
907
930
|
end
|
|
908
931
|
|
|
932
|
+
# Main checkout: the databases ARE the app's own — no suffix, no
|
|
933
|
+
# .env files, no claim names. Just make sure they exist.
|
|
934
|
+
def db_create_main(rows)
|
|
935
|
+
missing = rows.reject { |r| Database.exists?(r["database"], r["url"]) }
|
|
936
|
+
failed = missing.filter_map do |r|
|
|
937
|
+
r["database"] unless Database.ensure_exists(r["database"], r["url"])
|
|
938
|
+
end
|
|
939
|
+
unless failed.empty?
|
|
940
|
+
$stderr.puts "Error: could not create #{failed.join(', ')}. Check the database server."
|
|
941
|
+
exit 1
|
|
942
|
+
end
|
|
943
|
+
puts "Base databases ready (#{rows.size}):"
|
|
944
|
+
rows.each { |r| puts " #{r["database"]}" }
|
|
945
|
+
end
|
|
946
|
+
|
|
909
947
|
def db_drop(ctx, args)
|
|
910
948
|
force = args.delete("--force")
|
|
911
949
|
name = args.first
|
|
@@ -913,28 +951,69 @@ module Yamine
|
|
|
913
951
|
|
|
914
952
|
map = Database.load_map(ctx.store.dir)
|
|
915
953
|
info = map[name]
|
|
916
|
-
|
|
954
|
+
unless info
|
|
955
|
+
# A name from inside a claim identifies the claim — the set
|
|
956
|
+
# is the unit; half a suffixed set is orphan territory.
|
|
957
|
+
key, found = map.find { |_k, i| i.is_a?(Hash) && i.dig("names")&.value?(name) }
|
|
958
|
+
name, info = key, found if info.nil? && found
|
|
959
|
+
end
|
|
960
|
+
raise Error, "no claim named #{name.inspect} here — `yamine db list` shows what exists" unless info
|
|
961
|
+
|
|
962
|
+
if File.directory?(info["dir"]) && !force
|
|
917
963
|
$stderr.puts "Error: worktree #{info["dir"]} still exists. Use --force to drop #{name} anyway."
|
|
918
964
|
exit 1
|
|
919
965
|
end
|
|
966
|
+
|
|
967
|
+
pairs = Database.claim_pairs(info) || begin
|
|
968
|
+
template = drop_template
|
|
969
|
+
unless template && !template.strip.empty?
|
|
970
|
+
$stderr.puts "Error: no DATABASE_URL template found (ENV or env.clear in config/local.yml) — cannot reach the server to drop #{name}."
|
|
971
|
+
exit 1
|
|
972
|
+
end
|
|
973
|
+
[[name, template]]
|
|
974
|
+
end
|
|
920
975
|
template = drop_template
|
|
921
|
-
|
|
922
|
-
|
|
976
|
+
pairs = pairs.map { |db, tmpl| [db, (tmpl || template)] }
|
|
977
|
+
|
|
978
|
+
failed = false
|
|
979
|
+
pairs.each do |db, tmpl|
|
|
980
|
+
case Database.drop(db, tmpl)
|
|
981
|
+
when :dropped then puts " dropped #{db}"
|
|
982
|
+
when :missing then puts " #{db} did not exist — nothing to drop"
|
|
983
|
+
when :failed then failed = true
|
|
984
|
+
end
|
|
985
|
+
end
|
|
986
|
+
if failed
|
|
987
|
+
$stderr.puts "Error: could not drop part of #{name} — is the database server running? The claim is kept."
|
|
923
988
|
exit 1
|
|
924
989
|
end
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
990
|
+
|
|
991
|
+
map.delete(name)
|
|
992
|
+
Database.save_map(ctx.store.dir, map)
|
|
993
|
+
puts "Dropped #{name}."
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
# The probe exposed: what THIS app, right here, resolves to —
|
|
997
|
+
# database.yml + credentials and all, asked inside the app so no
|
|
998
|
+
# key or parser is needed outside it. Passwords masked; this
|
|
999
|
+
# prints to terminals and CI logs.
|
|
1000
|
+
def db_describe(_ctx)
|
|
1001
|
+
rows = Probe.rails_databases(Dir.pwd)
|
|
1002
|
+
unless rows
|
|
1003
|
+
$stderr.puts "Error: could not ask this app for its databases."
|
|
1004
|
+
$stderr.puts " Is it a Rails app with a working `bin/rails runner`? (probe timed out or exited non-zero)"
|
|
936
1005
|
exit 1
|
|
937
1006
|
end
|
|
1007
|
+
rows.each do |r|
|
|
1008
|
+
kind = %i[postgres mysql].include?(Database.adapter_for(r["url"])) ? "server" : "local"
|
|
1009
|
+
puts " #{r["name"]} (#{kind})"
|
|
1010
|
+
puts " database: #{r["database"]}"
|
|
1011
|
+
puts " url: #{mask_url(r["url"])}"
|
|
1012
|
+
end
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
def mask_url(url)
|
|
1016
|
+
url.to_s.sub(%r{\A([a-z0-9+]+://[^:/@]+):[^@]*@}, '\1:***@')
|
|
938
1017
|
end
|
|
939
1018
|
|
|
940
1019
|
# Template for reaching the database server outside of boot:
|
data/lib/yamine/cli/worktree.rb
CHANGED
|
@@ -25,9 +25,28 @@ module Yamine
|
|
|
25
25
|
module_function
|
|
26
26
|
|
|
27
27
|
# Per-checkout config that git does not carry: a fresh worktree
|
|
28
|
-
# boots without these
|
|
29
|
-
#
|
|
28
|
+
# boots without these — missing secrets, no proxy config, and (for
|
|
29
|
+
# credentials apps) no decryption key, which stops Rails before it
|
|
30
|
+
# can even read database.yml. The single largest follow-up cost of
|
|
31
|
+
# a new worktree, now carried automatically.
|
|
30
32
|
LOCAL_CONFIG_FILES = %w[config/local.yml config/local.secrets].freeze
|
|
33
|
+
# Only the credential keys the worktree's own environments need:
|
|
34
|
+
# master (multi-env credentials), development (boots + probe),
|
|
35
|
+
# test (test runs). Production and staging keys have no business
|
|
36
|
+
# sitting in a throwaway worktree directory.
|
|
37
|
+
CREDENTIAL_KEY_FILES = %w[
|
|
38
|
+
config/master.key
|
|
39
|
+
config/credentials/development.key
|
|
40
|
+
config/credentials/test.key
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
# Everything yamine copies or writes that must never read as
|
|
44
|
+
# uncommitted work: the per-checkout config, the three allowed
|
|
45
|
+
# keys, and the generated environment files.
|
|
46
|
+
def local_config_files(_dir)
|
|
47
|
+
LOCAL_CONFIG_FILES + CREDENTIAL_KEY_FILES + Database::ENV_FILES +
|
|
48
|
+
[Database::LEGACY_MARKER_FILE]
|
|
49
|
+
end
|
|
31
50
|
|
|
32
51
|
def run(ctx, args)
|
|
33
52
|
sub = args.first
|
|
@@ -52,7 +71,10 @@ module Yamine
|
|
|
52
71
|
default_branch = Worktrees.default_branch(repo)
|
|
53
72
|
Worktrees.list(repo).each do |entry|
|
|
54
73
|
next if entry.bare
|
|
55
|
-
|
|
74
|
+
|
|
75
|
+
claim = claim_for(map, entry.path)
|
|
76
|
+
puts " #{entry_line(entry, claim, repo, default_branch)}"
|
|
77
|
+
print_claim_databases(claim)
|
|
56
78
|
printed = true
|
|
57
79
|
end
|
|
58
80
|
end
|
|
@@ -66,14 +88,17 @@ module Yamine
|
|
|
66
88
|
def entry_line(entry, claim, repo, default_branch)
|
|
67
89
|
name = entry.main? ? "main" : (entry.branch || "(detached)")
|
|
68
90
|
line = "#{name} #{entry.path}"
|
|
69
|
-
|
|
91
|
+
if claim
|
|
92
|
+
names = claim[1].is_a?(Hash) ? claim[1]["names"] : nil
|
|
93
|
+
line += names && !names.empty? ? " db #{names.size} databases" : " db #{claim[0]}"
|
|
94
|
+
end
|
|
70
95
|
|
|
71
96
|
flags = []
|
|
72
97
|
flags << "main checkout" if entry.main?
|
|
73
98
|
flags << "detached" if entry.detached && !entry.main?
|
|
74
99
|
flags << "dir gone" unless entry.exists?
|
|
75
100
|
if !entry.main? && entry.exists?
|
|
76
|
-
flags << "dirty" if Worktrees.dirty?(entry.path, ignore:
|
|
101
|
+
flags << "dirty" if Worktrees.dirty?(entry.path, ignore: local_config_files(entry.path))
|
|
77
102
|
if entry.detached
|
|
78
103
|
flags << "unmerged"
|
|
79
104
|
else
|
|
@@ -85,6 +110,18 @@ module Yamine
|
|
|
85
110
|
line
|
|
86
111
|
end
|
|
87
112
|
|
|
113
|
+
# Every database a claim stands for, one per line under the row —
|
|
114
|
+
# five names inline would unreadable, and hiding them would
|
|
115
|
+
# re-create the "which database is this worktree even using"
|
|
116
|
+
# question the claim exists to answer.
|
|
117
|
+
def print_claim_databases(claim)
|
|
118
|
+
info = claim && claim[1]
|
|
119
|
+
names = info.is_a?(Hash) ? info["names"] : nil
|
|
120
|
+
return unless names.is_a?(Hash) && !names.empty?
|
|
121
|
+
|
|
122
|
+
names.each_value { |db| puts " db: #{db}" }
|
|
123
|
+
end
|
|
124
|
+
|
|
88
125
|
# yamine worktree add <name> [--dir <path>] [--no-install]
|
|
89
126
|
#
|
|
90
127
|
# `name` is the branch (feature/login stays feature/login — the
|
|
@@ -135,13 +172,16 @@ module Yamine
|
|
|
135
172
|
|
|
136
173
|
def copy_local_config(from_dir, to_dir)
|
|
137
174
|
copied = []
|
|
138
|
-
|
|
175
|
+
local_config_files(from_dir).each do |rel|
|
|
139
176
|
src = File.join(from_dir, rel)
|
|
140
177
|
next unless File.file?(src)
|
|
141
178
|
|
|
142
179
|
dst = File.join(to_dir, rel)
|
|
143
180
|
FileUtils.mkdir_p(File.dirname(dst))
|
|
144
181
|
FileUtils.cp(src, dst)
|
|
182
|
+
# Decryption keys stay as private in the worktree as they
|
|
183
|
+
# were in the checkout — FileUtils.cp does not carry modes.
|
|
184
|
+
File.chmod(0o600, dst) if rel.end_with?(".key")
|
|
145
185
|
copied << rel
|
|
146
186
|
end
|
|
147
187
|
copied
|
|
@@ -158,27 +198,36 @@ module Yamine
|
|
|
158
198
|
puts " bundle install did not finish — run it in #{dir} before booting" unless $?.success?
|
|
159
199
|
end
|
|
160
200
|
|
|
161
|
-
# Claim
|
|
201
|
+
# Claim this worktree's databases and create them now (with the
|
|
162
202
|
# app's schema) so server problems surface at creation time, not
|
|
163
|
-
# at first boot.
|
|
164
|
-
#
|
|
165
|
-
#
|
|
203
|
+
# at first boot. Rails apps are ASKED what they have (the probe
|
|
204
|
+
# resolves database.yml + credentials inside the app itself) and
|
|
205
|
+
# get the whole suffixed set — claim, .env files, databases,
|
|
206
|
+
# schema. Boot reuses an existing set idempotently, so this is
|
|
207
|
+
# never wasted work. SQLite and non-Rails apps fall through to
|
|
208
|
+
# the single-database path, which setup_database narrates.
|
|
166
209
|
def prepare_database(ctx, dir)
|
|
167
210
|
return nil unless Config.load(dir)
|
|
168
211
|
|
|
169
|
-
|
|
212
|
+
claim_key = Database.name_for(dir, env: BootCommand.rails_env,
|
|
170
213
|
state_dir: ctx.store.dir)
|
|
171
|
-
|
|
172
|
-
|
|
214
|
+
rows = Probe.rails_databases(dir)
|
|
215
|
+
server = rows && Probe.server_backed(rows)
|
|
216
|
+
if server && !server.empty?
|
|
217
|
+
BootCommand.provision_multidb(ctx, dir, claim_key, server)
|
|
218
|
+
else
|
|
219
|
+
Dir.chdir(dir) do
|
|
220
|
+
BootCommand.setup_database(ctx, Resolver.resolve(dir), claim_key)
|
|
221
|
+
end
|
|
173
222
|
end
|
|
174
|
-
|
|
223
|
+
claim_key
|
|
175
224
|
end
|
|
176
225
|
|
|
177
|
-
def say_ready(ctx, dir, name,
|
|
226
|
+
def say_ready(ctx, dir, name, db_key)
|
|
178
227
|
puts "Worktree ready:"
|
|
179
228
|
puts " dir #{dir}"
|
|
180
229
|
puts " branch #{name}"
|
|
181
|
-
|
|
230
|
+
print_ready_databases(ctx, db_key)
|
|
182
231
|
if (resolved = resolved_quietly(dir))
|
|
183
232
|
Resolver.urls(resolved, port: ctx.proxy_port, tls: ctx.proxy_tls)
|
|
184
233
|
.each { |u| puts " url #{u}" }
|
|
@@ -186,6 +235,18 @@ module Yamine
|
|
|
186
235
|
puts " boot it: cd #{dir} && yamine start"
|
|
187
236
|
end
|
|
188
237
|
|
|
238
|
+
def print_ready_databases(ctx, db_key)
|
|
239
|
+
return unless db_key
|
|
240
|
+
|
|
241
|
+
names = Database.load_map(ctx.store.dir).dig(db_key, "names")
|
|
242
|
+
if names.is_a?(Hash) && !names.empty?
|
|
243
|
+
puts " db #{names.size} databases:"
|
|
244
|
+
names.each_value { |db| puts " #{db}" }
|
|
245
|
+
else
|
|
246
|
+
puts " db #{db_key}"
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
189
250
|
def resolved_quietly(dir)
|
|
190
251
|
Resolver.resolve(dir)
|
|
191
252
|
rescue StandardError
|
|
@@ -214,7 +275,7 @@ module Yamine
|
|
|
214
275
|
return
|
|
215
276
|
end
|
|
216
277
|
|
|
217
|
-
if Worktrees.dirty?(entry.path, ignore:
|
|
278
|
+
if Worktrees.dirty?(entry.path, ignore: local_config_files(entry.path)) && !opts[:force]
|
|
218
279
|
$stderr.puts "Error: #{entry.path} has uncommitted changes."
|
|
219
280
|
$stderr.puts " Commit or stash them, or re-run with --force to discard them."
|
|
220
281
|
exit 1
|
|
@@ -309,7 +370,7 @@ module Yamine
|
|
|
309
370
|
plan << Plan.new(:prune, entry)
|
|
310
371
|
next
|
|
311
372
|
end
|
|
312
|
-
if Worktrees.dirty?(entry.path, ignore:
|
|
373
|
+
if Worktrees.dirty?(entry.path, ignore: local_config_files(entry.path))
|
|
313
374
|
plan << Plan.new(:keep, entry.path, "uncommitted changes are never cleaned automatically")
|
|
314
375
|
next
|
|
315
376
|
end
|
|
@@ -328,29 +389,55 @@ module Yamine
|
|
|
328
389
|
end
|
|
329
390
|
|
|
330
391
|
# An admin entry whose directory vanished (rm -rf, a crashed
|
|
331
|
-
# agent): drop
|
|
332
|
-
# entry itself is cleared by the trailing prune.
|
|
333
|
-
#
|
|
334
|
-
#
|
|
392
|
+
# agent): drop every database it claimed, forget the claim. The
|
|
393
|
+
# git entry itself is cleared by the trailing prune. The claim
|
|
394
|
+
# itself carries the names AND the server coordinates — an
|
|
395
|
+
# orphan's config is unreadable by definition, and the app must
|
|
396
|
+
# not have to boot (or even still bundle) for cleanup to work.
|
|
397
|
+
# A claim with no server behind it (sqlite) means there is
|
|
398
|
+
# nothing to drop and no retry that could help — the claim is
|
|
399
|
+
# simply forgotten. A LEGACY claim with no reachable template
|
|
400
|
+
# anywhere (credentials apps pre-multi-database never declared
|
|
401
|
+
# one) is also forgotten, with the database name said out loud:
|
|
402
|
+
# erroring forever would leave `clean` permanently red for a
|
|
403
|
+
# claim no future command can ever resolve.
|
|
335
404
|
def drop_orphan(ctx, name, _dir)
|
|
405
|
+
info = Database.load_map(ctx.store.dir)[name]
|
|
406
|
+
pairs = Database.claim_pairs(info)
|
|
336
407
|
template = orphan_template(ctx)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
408
|
+
|
|
409
|
+
if pairs.nil?
|
|
410
|
+
unless template
|
|
411
|
+
warn " no DATABASE_URL template found (ENV or env.clear in config/local.yml) — " \
|
|
412
|
+
"cannot reach #{name}; claim forgotten."
|
|
413
|
+
warn " If a database named #{name} still exists on a server, drop it manually."
|
|
414
|
+
forget_claim(ctx, name)
|
|
415
|
+
return true
|
|
416
|
+
end
|
|
417
|
+
pairs = [[name, template]]
|
|
418
|
+
else
|
|
419
|
+
pairs = pairs.map { |db, tmpl| [db, (tmpl || template)] }
|
|
341
420
|
end
|
|
342
|
-
|
|
421
|
+
|
|
422
|
+
first_url = pairs.first&.[](1)
|
|
423
|
+
unless %i[postgres mysql].include?(first_url && Database.adapter_for(first_url))
|
|
343
424
|
puts " #{name} has no server database (sqlite/unknown adapter) — claim forgotten"
|
|
344
425
|
forget_claim(ctx, name)
|
|
345
426
|
return true
|
|
346
427
|
end
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
428
|
+
|
|
429
|
+
failed = false
|
|
430
|
+
pairs.each do |db, tmpl|
|
|
431
|
+
case Database.drop(db, tmpl)
|
|
432
|
+
when :dropped then puts " dropped #{db}"
|
|
433
|
+
when :missing then puts " #{db} did not exist — nothing to drop"
|
|
434
|
+
when :failed
|
|
435
|
+
warn " could not drop #{db} — is the server running? Claim kept for a retry."
|
|
436
|
+
failed = true
|
|
437
|
+
end
|
|
353
438
|
end
|
|
439
|
+
return false if failed
|
|
440
|
+
|
|
354
441
|
forget_claim(ctx, name)
|
|
355
442
|
true
|
|
356
443
|
end
|
|
@@ -419,7 +506,7 @@ module Yamine
|
|
|
419
506
|
stop_routes(ctx, dir)
|
|
420
507
|
|
|
421
508
|
if claim
|
|
422
|
-
outcome = drop_database(repo, dir, claim[0])
|
|
509
|
+
outcome = drop_database(repo, dir, claim[0], claim[1])
|
|
423
510
|
if outcome == :failed
|
|
424
511
|
warn " kept #{dir} — database drop failed; fix the server and re-run"
|
|
425
512
|
return false
|
|
@@ -476,22 +563,35 @@ module Yamine
|
|
|
476
563
|
end
|
|
477
564
|
end
|
|
478
565
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
566
|
+
# Drop every database a claim stands for: the whole suffixed set
|
|
567
|
+
# for a multi claim, the one key-named database for a legacy
|
|
568
|
+
# single claim. Any :failed aborts the whole teardown (kept for
|
|
569
|
+
# a retry) — a half-dropped set with a removed worktree is
|
|
570
|
+
# orphan territory, and orphans are the bug this guards.
|
|
571
|
+
def drop_database(repo, dir, name, info = nil)
|
|
572
|
+
fallback = drop_template_for(repo, dir)
|
|
573
|
+
pairs = Database.claim_pairs(info) || [[name, fallback]]
|
|
574
|
+
pairs = pairs.map { |db, tmpl| [db, (tmpl || fallback)] }
|
|
575
|
+
|
|
576
|
+
first_url = pairs.first&.[](1)
|
|
577
|
+
unless %i[postgres mysql].include?(first_url && Database.adapter_for(first_url))
|
|
483
578
|
puts " no server database to drop for #{name} " \
|
|
484
|
-
"(#{
|
|
579
|
+
"(#{first_url ? "sqlite/unknown adapter" : "no DATABASE_URL template"})"
|
|
485
580
|
return :missing
|
|
486
581
|
end
|
|
487
582
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
583
|
+
outcomes = pairs.map do |db, tmpl|
|
|
584
|
+
outcome = Database.drop(db, tmpl)
|
|
585
|
+
case outcome
|
|
586
|
+
when :dropped then puts " dropped #{db}"
|
|
587
|
+
when :missing then puts " #{db} did not exist — nothing to drop"
|
|
588
|
+
when :failed then puts " could not drop #{db} — is the database server running?"
|
|
589
|
+
end
|
|
590
|
+
outcome
|
|
493
591
|
end
|
|
494
|
-
|
|
592
|
+
return :failed if outcomes.include?(:failed)
|
|
593
|
+
|
|
594
|
+
outcomes.include?(:dropped) ? :dropped : :missing
|
|
495
595
|
end
|
|
496
596
|
|
|
497
597
|
# Reach the database server from the worktree's own config (read
|
data/lib/yamine/cli.rb
CHANGED
|
@@ -94,7 +94,7 @@ module Yamine
|
|
|
94
94
|
yamine trust Add local CA to trust store
|
|
95
95
|
yamine clean Remove state and hosts entries
|
|
96
96
|
yamine prune Remove stale routes
|
|
97
|
-
yamine db list|create|drop
|
|
97
|
+
yamine db list|create|drop|describe Per-worktree databases (multi-database aware)
|
|
98
98
|
yamine worktree list|add|remove|clean Worktrees: create, inspect, full cleanup
|
|
99
99
|
yamine skills install Install yamine skill into ~/.agents/skills/ (or --local)
|
|
100
100
|
yamine proxy start|stop Control the proxy
|