nvoi 0.1.8 → 0.2.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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -5
  3. data/Gemfile.lock +17 -8
  4. data/Rakefile +1 -1
  5. data/lib/nvoi/cli/config/command.rb +46 -41
  6. data/lib/nvoi/cli/credentials/edit/command.rb +20 -20
  7. data/lib/nvoi/cli/credentials/show/command.rb +1 -1
  8. data/lib/nvoi/cli/db/command.rb +10 -10
  9. data/lib/nvoi/cli/delete/command.rb +2 -2
  10. data/lib/nvoi/cli/deploy/command.rb +2 -2
  11. data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +2 -2
  12. data/lib/nvoi/cli/deploy/steps/provision_server.rb +1 -1
  13. data/lib/nvoi/cli/deploy/steps/provision_volume.rb +1 -1
  14. data/lib/nvoi/cli/exec/command.rb +3 -3
  15. data/lib/nvoi/cli/logs/command.rb +2 -2
  16. data/lib/nvoi/cli/onboard/command.rb +176 -622
  17. data/lib/nvoi/cli/onboard/steps/app.rb +108 -0
  18. data/lib/nvoi/cli/onboard/steps/app_name.rb +26 -0
  19. data/lib/nvoi/cli/onboard/steps/compute.rb +139 -0
  20. data/lib/nvoi/cli/onboard/steps/database.rb +97 -0
  21. data/lib/nvoi/cli/onboard/steps/domain.rb +48 -0
  22. data/lib/nvoi/cli/onboard/steps/env.rb +67 -0
  23. data/lib/nvoi/cli/onboard/ui.rb +84 -0
  24. data/lib/nvoi/cli/unlock/command.rb +2 -2
  25. data/lib/nvoi/cli.rb +0 -32
  26. data/lib/nvoi/configuration/app_service.rb +54 -0
  27. data/lib/nvoi/configuration/application.rb +44 -0
  28. data/lib/nvoi/configuration/builder.rb +417 -0
  29. data/lib/nvoi/configuration/database.rb +56 -0
  30. data/lib/nvoi/configuration/deploy.rb +15 -0
  31. data/lib/nvoi/{objects/service_spec.rb → configuration/deployment.rb} +4 -3
  32. data/lib/nvoi/{objects/config_override.rb → configuration/override.rb} +4 -4
  33. data/lib/nvoi/configuration/providers.rb +78 -0
  34. data/lib/nvoi/configuration/result.rb +43 -0
  35. data/lib/nvoi/configuration/root.rb +234 -0
  36. data/lib/nvoi/configuration/server.rb +39 -0
  37. data/lib/nvoi/configuration/service.rb +62 -0
  38. data/lib/nvoi/external/cloud/aws.rb +12 -12
  39. data/lib/nvoi/external/cloud/hetzner.rb +7 -7
  40. data/lib/nvoi/external/cloud/scaleway.rb +7 -7
  41. data/lib/nvoi/external/cloud/types.rb +42 -0
  42. data/lib/nvoi/external/database/mysql.rb +1 -1
  43. data/lib/nvoi/external/database/postgres.rb +1 -1
  44. data/lib/nvoi/external/database/provider.rb +1 -1
  45. data/lib/nvoi/external/database/sqlite.rb +1 -1
  46. data/lib/nvoi/external/database/types.rb +55 -0
  47. data/lib/nvoi/external/dns/cloudflare.rb +6 -6
  48. data/lib/nvoi/external/dns/types.rb +24 -0
  49. data/lib/nvoi/utils/config_loader.rb +12 -12
  50. data/lib/nvoi/utils/credential_store.rb +4 -4
  51. data/lib/nvoi/utils/env_resolver.rb +3 -3
  52. data/lib/nvoi/utils/namer.rb +2 -2
  53. data/lib/nvoi/utils/presence.rb +23 -0
  54. data/lib/nvoi/version.rb +1 -1
  55. data/lib/nvoi.rb +2 -17
  56. metadata +95 -58
  57. data/.claude/todo/refactor/00-overview.md +0 -171
  58. data/.claude/todo/refactor/01-objects.md +0 -96
  59. data/.claude/todo/refactor/02-utils.md +0 -143
  60. data/.claude/todo/refactor/03-external-cloud.md +0 -164
  61. data/.claude/todo/refactor/04-external-dns.md +0 -104
  62. data/.claude/todo/refactor/05-external.md +0 -133
  63. data/.claude/todo/refactor/06-cli.md +0 -123
  64. data/.claude/todo/refactor/07-cli-deploy-command.md +0 -177
  65. data/.claude/todo/refactor/08-cli-deploy-steps.md +0 -201
  66. data/.claude/todo/refactor/09-cli-delete-command.md +0 -169
  67. data/.claude/todo/refactor/10-cli-exec-command.md +0 -157
  68. data/.claude/todo/refactor/11-cli-credentials-command.md +0 -190
  69. data/.claude/todo/refactor/12-cli-db-command.md +0 -128
  70. data/.claude/todo/refactor/_target.md +0 -79
  71. data/.claude/todo/refactor-execution/00-entrypoint.md +0 -49
  72. data/.claude/todo/refactor-execution/01-objects.md +0 -42
  73. data/.claude/todo/refactor-execution/02-utils.md +0 -41
  74. data/.claude/todo/refactor-execution/03-external-cloud.md +0 -38
  75. data/.claude/todo/refactor-execution/04-external-dns.md +0 -35
  76. data/.claude/todo/refactor-execution/05-external-other.md +0 -46
  77. data/.claude/todo/refactor-execution/06-cli-deploy.md +0 -45
  78. data/.claude/todo/refactor-execution/07-cli-delete.md +0 -43
  79. data/.claude/todo/refactor-execution/08-cli-exec.md +0 -30
  80. data/.claude/todo/refactor-execution/09-cli-credentials.md +0 -34
  81. data/.claude/todo/refactor-execution/10-cli-db.md +0 -31
  82. data/.claude/todo/refactor-execution/11-cli-router.md +0 -44
  83. data/.claude/todo/refactor-execution/12-cleanup.md +0 -120
  84. data/.claude/todo/refactor-execution/_monitoring-strategy.md +0 -126
  85. data/.claude/todo/scaleway.impl.md +0 -644
  86. data/.claude/todo/scaleway.reference.md +0 -520
  87. data/.claude/todos/buckets.md +0 -41
  88. data/.claude/todos.md +0 -550
  89. data/ingest +0 -0
  90. data/lib/nvoi/config_api/actions/app.rb +0 -53
  91. data/lib/nvoi/config_api/actions/compute_provider.rb +0 -55
  92. data/lib/nvoi/config_api/actions/database.rb +0 -70
  93. data/lib/nvoi/config_api/actions/domain_provider.rb +0 -40
  94. data/lib/nvoi/config_api/actions/env.rb +0 -32
  95. data/lib/nvoi/config_api/actions/init.rb +0 -67
  96. data/lib/nvoi/config_api/actions/secret.rb +0 -32
  97. data/lib/nvoi/config_api/actions/server.rb +0 -66
  98. data/lib/nvoi/config_api/actions/service.rb +0 -52
  99. data/lib/nvoi/config_api/actions/volume.rb +0 -40
  100. data/lib/nvoi/config_api/base.rb +0 -38
  101. data/lib/nvoi/config_api/result.rb +0 -26
  102. data/lib/nvoi/config_api.rb +0 -93
  103. data/lib/nvoi/objects/configuration.rb +0 -483
  104. data/lib/nvoi/objects/database.rb +0 -56
  105. data/lib/nvoi/objects/dns.rb +0 -14
  106. data/lib/nvoi/objects/firewall.rb +0 -11
  107. data/lib/nvoi/objects/network.rb +0 -11
  108. data/lib/nvoi/objects/server.rb +0 -14
  109. data/lib/nvoi/objects/tunnel.rb +0 -14
  110. data/lib/nvoi/objects/volume.rb +0 -17
data/.claude/todos.md DELETED
@@ -1,550 +0,0 @@
1
- # Onboard Wizard: Multi-Server & Volume Validation
2
-
3
- ## Overview
4
-
5
- Extend the onboard wizard to support multi-server setups and add validation for volume/server constraints.
6
-
7
- ---
8
-
9
- ## Part 1: Volume/Server Validation
10
-
11
- ### File: `lib/nvoi/objects/configuration.rb`
12
-
13
- Add validation in `validate_servers_and_references` method (after line ~108):
14
-
15
- ```ruby
16
- # Validate volume mount constraints
17
- def validate_volume_mounts(app, servers)
18
- app.app.each do |app_name, app_config|
19
- next if app_config.mounts.nil? || app_config.mounts.empty?
20
-
21
- # Can't mount volumes when running on multiple servers
22
- if app_config.servers.size > 1
23
- raise Errors::ConfigValidationError,
24
- "app.#{app_name}: cannot mount volumes when running on multiple servers (#{app_config.servers.join(', ')})"
25
- end
26
-
27
- # Can't mount volumes on multi-instance server (count > 1)
28
- app_config.servers.each do |server_ref|
29
- server = servers[server_ref]
30
- next unless server
31
-
32
- if server.count && server.count > 1
33
- raise Errors::ConfigValidationError,
34
- "app.#{app_name}: cannot mount volumes on multi-instance server '#{server_ref}' (count: #{server.count})"
35
- end
36
-
37
- # Verify volume exists on server
38
- app_config.mounts.each_key do |vol_name|
39
- unless server.volumes&.key?(vol_name)
40
- available = server.volumes&.keys&.join(", ") || "none"
41
- raise Errors::ConfigValidationError,
42
- "app.#{app_name}: mount '#{vol_name}' not found on server '#{server_ref}' (available: #{available})"
43
- end
44
- end
45
- end
46
- end
47
-
48
- # Database always needs volume, validate server constraints
49
- return unless app.database
50
-
51
- app.database.servers.each do |server_ref|
52
- server = servers[server_ref]
53
- next unless server
54
-
55
- if server.count && server.count > 1
56
- raise Errors::ConfigValidationError,
57
- "database: cannot run on multi-instance server '#{server_ref}' (count: #{server.count}) - volumes can't multi-attach"
58
- end
59
- end
60
- end
61
- ```
62
-
63
- ### File: `test/nvoi/objects/config_test.rb`
64
-
65
- Add tests:
66
-
67
- ```ruby
68
- def test_validates_no_volume_mount_on_multi_instance_server
69
- config_data = {
70
- "application" => {
71
- "name" => "test",
72
- "servers" => {
73
- "workers" => { "count" => 3, "volumes" => { "data" => { "size" => 10 } } }
74
- },
75
- "app" => {
76
- "web" => { "servers" => ["workers"], "mounts" => { "data" => "/app/data" } }
77
- }
78
- }
79
- }
80
-
81
- error = assert_raises(Nvoi::Errors::ConfigValidationError) do
82
- Nvoi::Objects::Configuration.new(config_data)
83
- end
84
- assert_match(/cannot mount volumes on multi-instance server/, error.message)
85
- end
86
-
87
- def test_validates_no_volume_mount_on_multiple_servers
88
- config_data = {
89
- "application" => {
90
- "name" => "test",
91
- "servers" => {
92
- "server1" => { "master" => true, "volumes" => { "data" => { "size" => 10 } } },
93
- "server2" => { "volumes" => { "data" => { "size" => 10 } } }
94
- },
95
- "app" => {
96
- "web" => { "servers" => ["server1", "server2"], "mounts" => { "data" => "/app/data" } }
97
- }
98
- }
99
- }
100
-
101
- error = assert_raises(Nvoi::Errors::ConfigValidationError) do
102
- Nvoi::Objects::Configuration.new(config_data)
103
- end
104
- assert_match(/cannot mount volumes when running on multiple servers/, error.message)
105
- end
106
-
107
- def test_validates_database_not_on_multi_instance_server
108
- config_data = {
109
- "application" => {
110
- "name" => "test",
111
- "servers" => {
112
- "workers" => { "master" => true, "count" => 2, "volumes" => { "pg_data" => { "size" => 10 } } }
113
- },
114
- "database" => {
115
- "servers" => ["workers"],
116
- "adapter" => "postgres",
117
- "secrets" => { "POSTGRES_DB" => "x", "POSTGRES_USER" => "x", "POSTGRES_PASSWORD" => "x" }
118
- }
119
- }
120
- }
121
-
122
- error = assert_raises(Nvoi::Errors::ConfigValidationError) do
123
- Nvoi::Objects::Configuration.new(config_data)
124
- end
125
- assert_match(/database: cannot run on multi-instance server/, error.message)
126
- end
127
-
128
- def test_validates_mount_volume_exists_on_server
129
- config_data = {
130
- "application" => {
131
- "name" => "test",
132
- "servers" => {
133
- "main" => { "master" => true } # no volumes defined
134
- },
135
- "app" => {
136
- "web" => { "servers" => ["main"], "mounts" => { "data" => "/app/data" } }
137
- }
138
- }
139
- }
140
-
141
- error = assert_raises(Nvoi::Errors::ConfigValidationError) do
142
- Nvoi::Objects::Configuration.new(config_data)
143
- end
144
- assert_match(/mount 'data' not found on server 'main'/, error.message)
145
- end
146
- ```
147
-
148
- ---
149
-
150
- ## Part 2: Onboard Wizard - Server Setup
151
-
152
- ### File: `lib/nvoi/cli/onboard/command.rb`
153
-
154
- #### 2.1 Add `step_servers` after `step_compute_provider`
155
-
156
- ```ruby
157
- def step_servers
158
- puts
159
- puts section("Server Configuration")
160
-
161
- mode = @prompt.select("Server setup:") do |menu|
162
- menu.choice "Single server (recommended for small apps)", :single
163
- menu.choice "Multi-server (master + workers)", :multi
164
- end
165
-
166
- case mode
167
- when :single
168
- setup_single_server
169
- when :multi
170
- setup_multi_server
171
- end
172
- end
173
-
174
- def setup_single_server
175
- @data["application"]["servers"] = {
176
- "main" => { "master" => true, "count" => 1 }
177
- }
178
- @server_mode = :single
179
- end
180
-
181
- def setup_multi_server
182
- @data["application"]["servers"] = {}
183
-
184
- # Master server (control plane)
185
- puts
186
- puts pastel.dim("Master server runs k3s control plane")
187
- master_type = prompt_server_type("Master server type:")
188
- @data["application"]["servers"]["master"] = {
189
- "master" => true,
190
- "count" => 1,
191
- "type" => master_type
192
- }
193
-
194
- # Worker servers
195
- puts
196
- puts pastel.dim("Worker servers run your apps")
197
- worker_type = prompt_server_type("Worker server type:")
198
- worker_count = @prompt.ask("Number of workers:", default: "2", convert: :int)
199
- @data["application"]["servers"]["workers"] = {
200
- "count" => worker_count,
201
- "type" => worker_type
202
- }
203
-
204
- # Dedicated database server?
205
- if @prompt.yes?("Dedicated database server? (recommended for production)")
206
- db_type = prompt_server_type("Database server type:")
207
- @data["application"]["servers"]["database"] = {
208
- "count" => 1,
209
- "type" => db_type
210
- }
211
- @dedicated_db_server = true
212
- else
213
- @dedicated_db_server = false
214
- end
215
-
216
- @server_mode = :multi
217
- end
218
-
219
- def prompt_server_type(message)
220
- # Use cached server types from compute provider setup
221
- return @prompt.ask(message) unless @server_types
222
-
223
- choices = @server_types.map do |t|
224
- price = t[:price] ? " - #{t[:price]}/mo" : ""
225
- { name: "#{t[:name]} (#{t[:cores]} vCPU, #{t[:memory] / 1024}GB#{price})", value: t[:name] }
226
- end
227
- @prompt.select(message, choices, per_page: 10)
228
- end
229
- ```
230
-
231
- #### 2.2 Update `step_apps` - Add replicas and server selection
232
-
233
- ```ruby
234
- def step_apps
235
- puts
236
- puts section("Applications")
237
-
238
- @data["application"]["app"] ||= {}
239
-
240
- loop do
241
- name = @prompt.ask("App name:") { |q| q.required true }
242
- command = @prompt.ask("Run command (optional, leave blank for Docker entrypoint):")
243
- port = @prompt.ask("Port (optional, leave blank for background workers):")
244
- port = port.to_i if port && !port.to_s.empty?
245
-
246
- app_config = { "servers" => default_app_servers }
247
- app_config["command"] = command unless command.to_s.empty?
248
- app_config["port"] = port if port && port > 0
249
-
250
- # Replicas (only for web-facing apps)
251
- if port && port > 0
252
- replicas = @prompt.ask("Replicas:", default: "2", convert: :int)
253
- app_config["replicas"] = replicas if replicas && replicas > 0
254
- end
255
-
256
- # Server selection (only for multi-server)
257
- if @server_mode == :multi
258
- app_config["servers"] = prompt_server_selection(name, has_mounts: false)
259
- end
260
-
261
- # Domain selection only if port is set (web-facing) and Cloudflare configured
262
- if port && port > 0 && @cloudflare_zones&.any?
263
- domain, subdomain = prompt_domain_selection
264
- if domain
265
- app_config["domain"] = domain
266
- app_config["subdomain"] = subdomain unless subdomain.to_s.empty?
267
- end
268
- end
269
-
270
- pre_run = @prompt.ask("Pre-run command (e.g. migrations):")
271
- app_config["pre_run_command"] = pre_run unless pre_run.to_s.empty?
272
-
273
- @data["application"]["app"][name] = app_config
274
-
275
- break unless @prompt.yes?("Add another app?")
276
- end
277
- end
278
-
279
- def default_app_servers
280
- case @server_mode
281
- when :single then ["main"]
282
- when :multi then ["workers"]
283
- else ["main"]
284
- end
285
- end
286
-
287
- def prompt_server_selection(context, has_mounts: false)
288
- servers = @data["application"]["servers"].keys
289
-
290
- if has_mounts
291
- # Filter to single-instance servers only
292
- servers = servers.select do |name|
293
- count = @data["application"]["servers"][name]["count"] || 1
294
- count == 1
295
- end
296
-
297
- if servers.empty?
298
- error("No single-instance servers available for volume mounts")
299
- error("Volume mounts require count: 1 server (volumes can't multi-attach)")
300
- return ["main"]
301
- end
302
-
303
- # Single select only
304
- selected = @prompt.select("Server for #{context} (volumes require single server):", servers)
305
- [selected]
306
- else
307
- # Multi-select allowed for stateless apps
308
- @prompt.multi_select("Servers for #{context}:", servers, min: 1)
309
- end
310
- end
311
- ```
312
-
313
- #### 2.3 Update `step_database` - Server selection for multi-server
314
-
315
- ```ruby
316
- def step_database
317
- puts
318
- puts section("Database")
319
-
320
- adapter = @prompt.select("Database:") do |menu|
321
- menu.choice "PostgreSQL", "postgres"
322
- menu.choice "MySQL", "mysql"
323
- menu.choice "SQLite", "sqlite3"
324
- menu.choice "None (skip)", nil
325
- end
326
-
327
- return unless adapter
328
-
329
- # Database server selection for multi-server mode
330
- db_servers = if @server_mode == :multi
331
- if @dedicated_db_server
332
- ["database"] # Use dedicated db server
333
- else
334
- prompt_server_selection("database", has_mounts: true)
335
- end
336
- else
337
- ["main"]
338
- end
339
-
340
- db_config = {
341
- "servers" => db_servers,
342
- "adapter" => adapter
343
- }
344
-
345
- # Add volume to the database server
346
- db_server_name = db_servers.first
347
- @data["application"]["servers"][db_server_name]["volumes"] ||= {}
348
-
349
- case adapter
350
- when "postgres"
351
- db_name = @prompt.ask("Database name:", default: "#{@data["application"]["name"]}_production")
352
- user = @prompt.ask("Database user:", default: @data["application"]["name"])
353
- password = @prompt.mask("Database password:") { |q| q.required true }
354
-
355
- db_config["secrets"] = {
356
- "POSTGRES_DB" => db_name,
357
- "POSTGRES_USER" => user,
358
- "POSTGRES_PASSWORD" => password
359
- }
360
-
361
- @data["application"]["servers"][db_server_name]["volumes"]["postgres_data"] = { "size" => 10 }
362
-
363
- when "mysql"
364
- db_name = @prompt.ask("Database name:", default: "#{@data["application"]["name"]}_production")
365
- user = @prompt.ask("Database user:", default: @data["application"]["name"])
366
- password = @prompt.mask("Database password:") { |q| q.required true }
367
-
368
- db_config["secrets"] = {
369
- "MYSQL_DATABASE" => db_name,
370
- "MYSQL_USER" => user,
371
- "MYSQL_PASSWORD" => password
372
- }
373
-
374
- @data["application"]["servers"][db_server_name]["volumes"]["mysql_data"] = { "size" => 10 }
375
-
376
- when "sqlite3"
377
- path = @prompt.ask("Database path:", default: "/app/data/production.sqlite3")
378
- db_config["path"] = path
379
- db_config["mount"] = { "data" => "/app/data" }
380
-
381
- @data["application"]["servers"][db_server_name]["volumes"]["sqlite_data"] = { "size" => 10 }
382
- end
383
-
384
- @data["application"]["database"] = db_config
385
- end
386
- ```
387
-
388
- #### 2.4 Update `run` method
389
-
390
- ```ruby
391
- def run
392
- show_welcome
393
-
394
- step_app_name
395
- step_compute_provider
396
- step_servers # NEW - after compute provider
397
- step_domain_provider
398
- step_apps # Updated - replicas + server selection
399
- step_database # Updated - server selection
400
- step_env
401
-
402
- summary_loop
403
-
404
- show_next_steps
405
- rescue TTY::Reader::InputInterrupt
406
- puts "\n\nSetup cancelled."
407
- exit 1
408
- end
409
- ```
410
-
411
- #### 2.5 Update `show_summary` for multi-server
412
-
413
- ```ruby
414
- def show_summary
415
- # ... existing code ...
416
-
417
- # Server info
418
- server_info = @data["application"]["servers"].map do |name, cfg|
419
- count = cfg["count"] || 1
420
- type = cfg["type"] || "default"
421
- master = cfg["master"] ? " (master)" : ""
422
- "#{name}: #{count}x #{type}#{master}"
423
- end.join(", ")
424
-
425
- rows = [
426
- ["Application", @data["application"]["name"]],
427
- ["Provider", "#{provider_name} (#{provider_info})"],
428
- ["Servers", server_info], # NEW
429
- ["Domain", "Cloudflare #{domain_ok}"],
430
- ["Apps", app_list],
431
- ["Database", db],
432
- ["Env/Secrets", "#{env_count} variables"]
433
- ]
434
-
435
- # ... rest of method ...
436
- end
437
- ```
438
-
439
- ---
440
-
441
- ## Part 3: Test Updates
442
-
443
- ### File: `test/cli/onboard/test_command.rb`
444
-
445
- ```ruby
446
- def test_single_server_mode
447
- prompt = TTY::Prompt::Test.new
448
-
449
- prompt.input << "myapp\n"
450
- prompt.input << "\r" # hetzner
451
- prompt.input << "token\n"
452
- prompt.input << "\r" # server type
453
- prompt.input << "\r" # location
454
- prompt.input << "\r" # Single server mode
455
- prompt.input << "n\n" # no cloudflare
456
- prompt.input << "web\n"
457
- prompt.input << "\n" # no command
458
- prompt.input << "3000\n" # port
459
- prompt.input << "2\n" # replicas
460
- prompt.input << "\n" # no pre-run
461
- prompt.input << "n\n" # no more apps
462
- prompt.input << "\e[B\e[B\e[B\r" # no database
463
- prompt.input << "\e[B\e[B\r" # done with env
464
- prompt.input << "\e[B\e[B\e[B\e[B\e[B\e[B\e[B\e[B\r" # Cancel
465
- prompt.input << "y\n"
466
- prompt.input.rewind
467
-
468
- with_hetzner_mock do
469
- cmd = Nvoi::Cli::Onboard::Command.new(prompt:)
470
- cmd.run
471
- end
472
-
473
- output = prompt.output.string
474
- assert_match(/Single server/, output)
475
- end
476
-
477
- def test_multi_server_mode
478
- prompt = TTY::Prompt::Test.new
479
-
480
- prompt.input << "myapp\n"
481
- prompt.input << "\r" # hetzner
482
- prompt.input << "token\n"
483
- prompt.input << "\r" # server type
484
- prompt.input << "\r" # location
485
- prompt.input << "\e[B\r" # Multi-server mode
486
- prompt.input << "\r" # master type
487
- prompt.input << "\r" # worker type
488
- prompt.input << "2\n" # worker count
489
- prompt.input << "y\n" # dedicated db server
490
- prompt.input << "\r" # db server type
491
- prompt.input << "n\n" # no cloudflare
492
- prompt.input << "web\n"
493
- prompt.input << "\n"
494
- prompt.input << "3000\n"
495
- prompt.input << "2\n" # replicas
496
- prompt.input << " \r" # select workers (space to select, enter to confirm)
497
- prompt.input << "\n"
498
- prompt.input << "n\n"
499
- prompt.input << "\r" # postgres
500
- prompt.input << "mydb\n"
501
- prompt.input << "myuser\n"
502
- prompt.input << "mypass\n"
503
- prompt.input << "\e[B\e[B\r" # done with env
504
- prompt.input << "\e[B\e[B\e[B\e[B\e[B\e[B\e[B\e[B\r"
505
- prompt.input << "y\n"
506
- prompt.input.rewind
507
-
508
- with_hetzner_mock do
509
- cmd = Nvoi::Cli::Onboard::Command.new(prompt:)
510
- cmd.run
511
- end
512
-
513
- output = prompt.output.string
514
- assert_match(/master/, output)
515
- assert_match(/workers/, output)
516
- assert_match(/database/, output)
517
- end
518
- ```
519
-
520
- ---
521
-
522
- ## Implementation Order
523
-
524
- 1. [ ] **Part 1: Validation** - Add volume/server validation to configuration.rb + tests
525
- 2. [ ] **Part 2.1: step_servers** - Add server setup step
526
- 3. [ ] **Part 2.2: step_apps update** - Add replicas + server selection
527
- 4. [ ] **Part 2.3: step_database update** - Add server selection
528
- 5. [ ] **Part 2.4: run method** - Wire up new step
529
- 6. [ ] **Part 2.5: show_summary** - Show server info
530
- 7. [ ] **Part 3: Tests** - Add tests for new flows
531
-
532
- ---
533
-
534
- ## Notes
535
-
536
- - Keep `@server_types` cached from compute provider setup for reuse
537
- - `@server_mode` tracks :single vs :multi for conditional prompts
538
- - `@dedicated_db_server` tracks if user chose dedicated db server
539
- - Volume auto-creation happens in step_database based on selected server
540
- - Multi-select uses `multi_select` with `min: 1`
541
- - Single-server mode remains the default (recommended path)
542
-
543
- ---
544
-
545
- ## Part 4: nvoi unlock command
546
-
547
- ✅ DONE - `lib/nvoi/cli/unlock/command.rb`
548
- - SSHs into server and removes lock file
549
- - Shows lock age before removing
550
- - Uses `namer.deployment_lock_file_path`
data/ingest DELETED
File without changes
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nvoi
4
- module ConfigApi
5
- module Actions
6
- class SetApp < Base
7
- protected
8
-
9
- def mutate(data, name:, servers:, domain: nil, subdomain: nil, port: nil, command: nil, pre_run_command: nil, env: nil, mounts: nil)
10
- raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
11
- raise ArgumentError, "servers is required" if servers.nil? || servers.empty?
12
- raise ArgumentError, "servers must be an array" unless servers.is_a?(Array)
13
-
14
- validate_server_refs(data, servers)
15
-
16
- app(data)["app"] ||= {}
17
- app(data)["app"][name.to_s] = {
18
- "servers" => servers.map(&:to_s),
19
- "domain" => domain,
20
- "subdomain" => subdomain,
21
- "port" => port,
22
- "command" => command,
23
- "pre_run_command" => pre_run_command,
24
- "env" => env,
25
- "mounts" => mounts
26
- }.compact
27
- end
28
-
29
- private
30
-
31
- def validate_server_refs(data, servers)
32
- defined = (app(data)["servers"] || {}).keys
33
- servers.each do |ref|
34
- raise Errors::ConfigValidationError, "server '#{ref}' not found" unless defined.include?(ref.to_s)
35
- end
36
- end
37
- end
38
-
39
- class DeleteApp < Base
40
- protected
41
-
42
- def mutate(data, name:)
43
- raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
44
-
45
- apps = app(data)["app"] || {}
46
- raise Errors::ConfigValidationError, "app '#{name}' not found" unless apps.key?(name.to_s)
47
-
48
- apps.delete(name.to_s)
49
- end
50
- end
51
- end
52
- end
53
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nvoi
4
- module ConfigApi
5
- module Actions
6
- class SetComputeProvider < Base
7
- PROVIDERS = %w[hetzner aws scaleway].freeze
8
-
9
- protected
10
-
11
- def mutate(data, provider:, **opts)
12
- raise ArgumentError, "provider is required" if provider.nil? || provider.to_s.empty?
13
- raise ArgumentError, "provider must be one of: #{PROVIDERS.join(', ')}" unless PROVIDERS.include?(provider.to_s)
14
-
15
- app(data)["compute_provider"] = { provider.to_s => build_config(provider.to_s, opts) }
16
- end
17
-
18
- private
19
-
20
- def build_config(provider, opts)
21
- case provider
22
- when "hetzner"
23
- {
24
- "api_token" => opts[:api_token],
25
- "server_type" => opts[:server_type],
26
- "server_location" => opts[:server_location]
27
- }.compact
28
- when "aws"
29
- {
30
- "access_key_id" => opts[:access_key_id],
31
- "secret_access_key" => opts[:secret_access_key],
32
- "region" => opts[:region],
33
- "instance_type" => opts[:instance_type]
34
- }.compact
35
- when "scaleway"
36
- {
37
- "secret_key" => opts[:secret_key],
38
- "project_id" => opts[:project_id],
39
- "zone" => opts[:zone],
40
- "server_type" => opts[:server_type]
41
- }.compact
42
- end
43
- end
44
- end
45
-
46
- class DeleteComputeProvider < Base
47
- protected
48
-
49
- def mutate(data, **)
50
- app(data)["compute_provider"] = {}
51
- end
52
- end
53
- end
54
- end
55
- end