nvoi 0.1.6 → 0.1.8

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/todo/refactor/12-cli-db-command.md +128 -0
  3. data/.claude/todo/refactor-execution/00-entrypoint.md +49 -0
  4. data/.claude/todo/refactor-execution/01-objects.md +42 -0
  5. data/.claude/todo/refactor-execution/02-utils.md +41 -0
  6. data/.claude/todo/refactor-execution/03-external-cloud.md +38 -0
  7. data/.claude/todo/refactor-execution/04-external-dns.md +35 -0
  8. data/.claude/todo/refactor-execution/05-external-other.md +46 -0
  9. data/.claude/todo/refactor-execution/06-cli-deploy.md +45 -0
  10. data/.claude/todo/refactor-execution/07-cli-delete.md +43 -0
  11. data/.claude/todo/refactor-execution/08-cli-exec.md +30 -0
  12. data/.claude/todo/refactor-execution/09-cli-credentials.md +34 -0
  13. data/.claude/todo/refactor-execution/10-cli-db.md +31 -0
  14. data/.claude/todo/refactor-execution/11-cli-router.md +44 -0
  15. data/.claude/todo/refactor-execution/12-cleanup.md +120 -0
  16. data/.claude/todo/refactor-execution/_monitoring-strategy.md +126 -0
  17. data/.claude/todos/buckets.md +41 -0
  18. data/.claude/todos.md +550 -0
  19. data/Gemfile +5 -0
  20. data/Gemfile.lock +35 -4
  21. data/Rakefile +1 -1
  22. data/ingest +0 -0
  23. data/lib/nvoi/cli/config/command.rb +219 -0
  24. data/lib/nvoi/cli/delete/steps/teardown_dns.rb +12 -11
  25. data/lib/nvoi/cli/deploy/command.rb +27 -11
  26. data/lib/nvoi/cli/deploy/steps/build_image.rb +48 -6
  27. data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +15 -13
  28. data/lib/nvoi/cli/deploy/steps/deploy_service.rb +8 -15
  29. data/lib/nvoi/cli/deploy/steps/setup_k3s.rb +10 -1
  30. data/lib/nvoi/cli/logs/command.rb +66 -0
  31. data/lib/nvoi/cli/onboard/command.rb +761 -0
  32. data/lib/nvoi/cli/unlock/command.rb +72 -0
  33. data/lib/nvoi/cli.rb +257 -0
  34. data/lib/nvoi/config_api/actions/app.rb +30 -30
  35. data/lib/nvoi/config_api/actions/compute_provider.rb +31 -31
  36. data/lib/nvoi/config_api/actions/database.rb +42 -42
  37. data/lib/nvoi/config_api/actions/domain_provider.rb +40 -0
  38. data/lib/nvoi/config_api/actions/env.rb +12 -12
  39. data/lib/nvoi/config_api/actions/init.rb +67 -0
  40. data/lib/nvoi/config_api/actions/secret.rb +12 -12
  41. data/lib/nvoi/config_api/actions/server.rb +35 -35
  42. data/lib/nvoi/config_api/actions/service.rb +52 -0
  43. data/lib/nvoi/config_api/actions/volume.rb +18 -18
  44. data/lib/nvoi/config_api/base.rb +15 -21
  45. data/lib/nvoi/config_api/result.rb +5 -5
  46. data/lib/nvoi/config_api.rb +51 -28
  47. data/lib/nvoi/external/cloud/aws.rb +26 -1
  48. data/lib/nvoi/external/cloud/hetzner.rb +27 -1
  49. data/lib/nvoi/external/cloud/scaleway.rb +32 -6
  50. data/lib/nvoi/external/containerd.rb +1 -44
  51. data/lib/nvoi/external/dns/cloudflare.rb +34 -16
  52. data/lib/nvoi/external/ssh.rb +0 -12
  53. data/lib/nvoi/external/ssh_tunnel.rb +100 -0
  54. data/lib/nvoi/objects/configuration.rb +20 -0
  55. data/lib/nvoi/utils/namer.rb +9 -0
  56. data/lib/nvoi/utils/retry.rb +1 -1
  57. data/lib/nvoi/version.rb +1 -1
  58. data/lib/nvoi.rb +16 -0
  59. data/templates/app-ingress.yaml.erb +3 -1
  60. metadata +27 -1
@@ -6,26 +6,26 @@ module Nvoi
6
6
  class SetEnv < Base
7
7
  protected
8
8
 
9
- def mutate(data, key:, value:)
10
- raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
11
- raise ArgumentError, "value is required" if value.nil?
9
+ def mutate(data, key:, value:)
10
+ raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
11
+ raise ArgumentError, "value is required" if value.nil?
12
12
 
13
- app(data)["env"] ||= {}
14
- app(data)["env"][key.to_s] = value.to_s
15
- end
13
+ app(data)["env"] ||= {}
14
+ app(data)["env"][key.to_s] = value.to_s
15
+ end
16
16
  end
17
17
 
18
18
  class DeleteEnv < Base
19
19
  protected
20
20
 
21
- def mutate(data, key:)
22
- raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
21
+ def mutate(data, key:)
22
+ raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
23
23
 
24
- env = app(data)["env"] || {}
25
- raise Errors::ConfigValidationError, "env '#{key}' not found" unless env.key?(key.to_s)
24
+ env = app(data)["env"] || {}
25
+ raise Errors::ConfigValidationError, "env '#{key}' not found" unless env.key?(key.to_s)
26
26
 
27
- env.delete(key.to_s)
28
- end
27
+ env.delete(key.to_s)
28
+ end
29
29
  end
30
30
  end
31
31
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module ConfigApi
5
+ module Actions
6
+ class Init
7
+ def call(name:, environment: "production")
8
+ raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
9
+
10
+ # Generate master key
11
+ master_key = Utils::Crypto.generate_key
12
+
13
+ # Generate SSH keypair (reuse existing utility)
14
+ private_key, public_key = Utils::ConfigLoader.generate_keypair
15
+
16
+ # Build initial config
17
+ config_data = {
18
+ "application" => {
19
+ "name" => name.to_s,
20
+ "environment" => environment.to_s,
21
+ "domain_provider" => {},
22
+ "compute_provider" => {},
23
+ "servers" => {},
24
+ "app" => {},
25
+ "services" => {},
26
+ "env" => {},
27
+ "secrets" => {},
28
+ "ssh_keys" => {
29
+ "private_key" => private_key,
30
+ "public_key" => public_key
31
+ }
32
+ }
33
+ }
34
+
35
+ # Encrypt config
36
+ yaml = YAML.dump(config_data)
37
+ encrypted_config = Utils::Crypto.encrypt(yaml, master_key)
38
+
39
+ InitResult.new(
40
+ config: encrypted_config,
41
+ master_key:,
42
+ ssh_public_key: public_key
43
+ )
44
+ rescue ArgumentError => e
45
+ InitResult.new(error_type: :invalid_args, error_message: e.message)
46
+ rescue Errors::ConfigError => e
47
+ InitResult.new(error_type: :config_error, error_message: e.message)
48
+ end
49
+ end
50
+
51
+ class InitResult
52
+ attr_reader :config, :master_key, :ssh_public_key, :error_type, :error_message
53
+
54
+ def initialize(config: nil, master_key: nil, ssh_public_key: nil, error_type: nil, error_message: nil)
55
+ @config = config
56
+ @master_key = master_key
57
+ @ssh_public_key = ssh_public_key
58
+ @error_type = error_type
59
+ @error_message = error_message
60
+ end
61
+
62
+ def success? = @error_type.nil?
63
+ def failure? = !success?
64
+ end
65
+ end
66
+ end
67
+ end
@@ -6,26 +6,26 @@ module Nvoi
6
6
  class SetSecret < Base
7
7
  protected
8
8
 
9
- def mutate(data, key:, value:)
10
- raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
11
- raise ArgumentError, "value is required" if value.nil?
9
+ def mutate(data, key:, value:)
10
+ raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
11
+ raise ArgumentError, "value is required" if value.nil?
12
12
 
13
- app(data)["secrets"] ||= {}
14
- app(data)["secrets"][key.to_s] = value.to_s
15
- end
13
+ app(data)["secrets"] ||= {}
14
+ app(data)["secrets"][key.to_s] = value.to_s
15
+ end
16
16
  end
17
17
 
18
18
  class DeleteSecret < Base
19
19
  protected
20
20
 
21
- def mutate(data, key:)
22
- raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
21
+ def mutate(data, key:)
22
+ raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
23
23
 
24
- secrets = app(data)["secrets"] || {}
25
- raise Errors::ConfigValidationError, "secret '#{key}' not found" unless secrets.key?(key.to_s)
24
+ secrets = app(data)["secrets"] || {}
25
+ raise Errors::ConfigValidationError, "secret '#{key}' not found" unless secrets.key?(key.to_s)
26
26
 
27
- secrets.delete(key.to_s)
28
- end
27
+ secrets.delete(key.to_s)
28
+ end
29
29
  end
30
30
  end
31
31
  end
@@ -6,60 +6,60 @@ module Nvoi
6
6
  class SetServer < Base
7
7
  protected
8
8
 
9
- def mutate(data, name:, master: false, type: nil, location: nil, count: 1)
10
- raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
11
- raise ArgumentError, "count must be positive" if count && count < 1
9
+ def mutate(data, name:, master: false, type: nil, location: nil, count: 1)
10
+ raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
11
+ raise ArgumentError, "count must be positive" if count && count < 1
12
12
 
13
- app(data)["servers"] ||= {}
14
- app(data)["servers"][name.to_s] = {
15
- "master" => master,
16
- "type" => type,
17
- "location" => location,
18
- "count" => count
19
- }.compact
20
- end
13
+ app(data)["servers"] ||= {}
14
+ app(data)["servers"][name.to_s] = {
15
+ "master" => master,
16
+ "type" => type,
17
+ "location" => location,
18
+ "count" => count
19
+ }.compact
20
+ end
21
21
  end
22
22
 
23
23
  class DeleteServer < Base
24
24
  protected
25
25
 
26
- def mutate(data, name:)
27
- raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
26
+ def mutate(data, name:)
27
+ raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
28
28
 
29
- servers = app(data)["servers"] || {}
30
- raise Errors::ConfigValidationError, "server '#{name}' not found" unless servers.key?(name.to_s)
29
+ servers = app(data)["servers"] || {}
30
+ raise Errors::ConfigValidationError, "server '#{name}' not found" unless servers.key?(name.to_s)
31
31
 
32
- servers.delete(name.to_s)
33
- end
32
+ servers.delete(name.to_s)
33
+ end
34
34
 
35
- def validate(data)
36
- check_orphaned_references(data)
37
- end
35
+ def validate(data)
36
+ check_orphaned_references(data)
37
+ end
38
38
 
39
39
  private
40
40
 
41
- def check_orphaned_references(data)
42
- servers = (app(data)["servers"] || {}).keys
41
+ def check_orphaned_references(data)
42
+ servers = (app(data)["servers"] || {}).keys
43
43
 
44
- (app(data)["app"] || {}).each do |svc_name, svc|
45
- (svc["servers"] || []).each do |ref|
46
- raise Errors::ConfigValidationError, "app.#{svc_name} references non-existent server '#{ref}'" unless servers.include?(ref)
44
+ (app(data)["app"] || {}).each do |svc_name, svc|
45
+ (svc["servers"] || []).each do |ref|
46
+ raise Errors::ConfigValidationError, "app.#{svc_name} references non-existent server '#{ref}'" unless servers.include?(ref)
47
+ end
47
48
  end
48
- end
49
49
 
50
- db = app(data)["database"]
51
- if db
52
- (db["servers"] || []).each do |ref|
53
- raise Errors::ConfigValidationError, "database references non-existent server '#{ref}'" unless servers.include?(ref)
50
+ db = app(data)["database"]
51
+ if db
52
+ (db["servers"] || []).each do |ref|
53
+ raise Errors::ConfigValidationError, "database references non-existent server '#{ref}'" unless servers.include?(ref)
54
+ end
54
55
  end
55
- end
56
56
 
57
- (app(data)["services"] || {}).each do |svc_name, svc|
58
- (svc["servers"] || []).each do |ref|
59
- raise Errors::ConfigValidationError, "services.#{svc_name} references non-existent server '#{ref}'" unless servers.include?(ref)
57
+ (app(data)["services"] || {}).each do |svc_name, svc|
58
+ (svc["servers"] || []).each do |ref|
59
+ raise Errors::ConfigValidationError, "services.#{svc_name} references non-existent server '#{ref}'" unless servers.include?(ref)
60
+ end
60
61
  end
61
62
  end
62
- end
63
63
  end
64
64
  end
65
65
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module ConfigApi
5
+ module Actions
6
+ class SetService < Base
7
+ protected
8
+
9
+ def mutate(data, name:, servers:, image:, port: nil, command: nil, env: nil, mount: 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
+ raise ArgumentError, "image is required" if image.nil? || image.to_s.empty?
14
+
15
+ validate_server_refs(data, servers)
16
+
17
+ app(data)["services"] ||= {}
18
+ app(data)["services"][name.to_s] = {
19
+ "servers" => servers.map(&:to_s),
20
+ "image" => image.to_s,
21
+ "port" => port,
22
+ "command" => command,
23
+ "env" => env,
24
+ "mount" => mount
25
+ }.compact
26
+ end
27
+
28
+ private
29
+
30
+ def validate_server_refs(data, servers)
31
+ defined = (app(data)["servers"] || {}).keys
32
+ servers.each do |ref|
33
+ raise Errors::ConfigValidationError, "server '#{ref}' not found" unless defined.include?(ref.to_s)
34
+ end
35
+ end
36
+ end
37
+
38
+ class DeleteService < Base
39
+ protected
40
+
41
+ def mutate(data, name:)
42
+ raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
43
+
44
+ services = app(data)["services"] || {}
45
+ raise Errors::ConfigValidationError, "service '#{name}' not found" unless services.key?(name.to_s)
46
+
47
+ services.delete(name.to_s)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -6,34 +6,34 @@ module Nvoi
6
6
  class SetVolume < Base
7
7
  protected
8
8
 
9
- def mutate(data, server:, name:, size: 10)
10
- raise ArgumentError, "server is required" if server.nil? || server.to_s.empty?
11
- raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
12
- raise ArgumentError, "size must be positive" if size && size < 1
9
+ def mutate(data, server:, name:, size: 10)
10
+ raise ArgumentError, "server is required" if server.nil? || server.to_s.empty?
11
+ raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
12
+ raise ArgumentError, "size must be positive" if size && size < 1
13
13
 
14
- servers = app(data)["servers"] ||= {}
15
- raise Errors::ConfigValidationError, "server '#{server}' not found" unless servers.key?(server.to_s)
14
+ servers = app(data)["servers"] ||= {}
15
+ raise Errors::ConfigValidationError, "server '#{server}' not found" unless servers.key?(server.to_s)
16
16
 
17
- servers[server.to_s]["volumes"] ||= {}
18
- servers[server.to_s]["volumes"][name.to_s] = { "size" => size }
19
- end
17
+ servers[server.to_s]["volumes"] ||= {}
18
+ servers[server.to_s]["volumes"][name.to_s] = { "size" => size }
19
+ end
20
20
  end
21
21
 
22
22
  class DeleteVolume < Base
23
23
  protected
24
24
 
25
- def mutate(data, server:, name:)
26
- raise ArgumentError, "server is required" if server.nil? || server.to_s.empty?
27
- raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
25
+ def mutate(data, server:, name:)
26
+ raise ArgumentError, "server is required" if server.nil? || server.to_s.empty?
27
+ raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
28
28
 
29
- servers = app(data)["servers"] || {}
30
- raise Errors::ConfigValidationError, "server '#{server}' not found" unless servers.key?(server.to_s)
29
+ servers = app(data)["servers"] || {}
30
+ raise Errors::ConfigValidationError, "server '#{server}' not found" unless servers.key?(server.to_s)
31
31
 
32
- volumes = servers[server.to_s]["volumes"] || {}
33
- raise Errors::ConfigValidationError, "volume '#{name}' not found on server '#{server}'" unless volumes.key?(name.to_s)
32
+ volumes = servers[server.to_s]["volumes"] || {}
33
+ raise Errors::ConfigValidationError, "volume '#{name}' not found on server '#{server}'" unless volumes.key?(name.to_s)
34
34
 
35
- volumes.delete(name.to_s)
36
- end
35
+ volumes.delete(name.to_s)
36
+ end
37
37
  end
38
38
  end
39
39
  end
@@ -2,23 +2,17 @@
2
2
 
3
3
  module Nvoi
4
4
  module ConfigApi
5
+ # Base class for config transformations.
6
+ # Accepts a Hash, returns a Hash. No crypto - caller handles that.
5
7
  class Base
6
- def initialize(encrypted_config, master_key)
7
- @encrypted_config = encrypted_config
8
- @master_key = master_key
8
+ def initialize(data)
9
+ @data = data
9
10
  end
10
11
 
11
12
  def call(**args)
12
- yaml = Utils::Crypto.decrypt(@encrypted_config, @master_key)
13
- @data = YAML.safe_load(yaml, permitted_classes: [Symbol])
14
-
15
13
  mutate(@data, **args)
16
14
  validate(@data)
17
-
18
- new_yaml = YAML.dump(@data)
19
- Result.success(Utils::Crypto.encrypt(new_yaml, @master_key))
20
- rescue Errors::DecryptionError, Errors::InvalidKeyError => e
21
- Result.failure(:decryption_error, e.message)
15
+ Result.success(@data)
22
16
  rescue Errors::ConfigValidationError => e
23
17
  Result.failure(:validation_error, e.message)
24
18
  rescue ArgumentError => e
@@ -27,18 +21,18 @@ module Nvoi
27
21
 
28
22
  protected
29
23
 
30
- def mutate(_data, **_args)
31
- raise NotImplementedError
32
- end
24
+ def mutate(_data, **_args)
25
+ raise NotImplementedError
26
+ end
33
27
 
34
- def validate(_data)
35
- # Subclasses can override to add validation
36
- # Default: no validation (lightweight actions like set_env don't need full config validation)
37
- end
28
+ def validate(_data)
29
+ # Subclasses can override to add validation
30
+ # Default: no validation (lightweight actions like set_env don't need full config validation)
31
+ end
38
32
 
39
- def app(data)
40
- data["application"] ||= {}
41
- end
33
+ def app(data)
34
+ data["application"] ||= {}
35
+ end
42
36
  end
43
37
  end
44
38
  end
@@ -3,18 +3,18 @@
3
3
  module Nvoi
4
4
  module ConfigApi
5
5
  class Result
6
- attr_reader :config, :error_type, :error_message
6
+ attr_reader :data, :error_type, :error_message
7
7
 
8
- def self.success(config)
9
- new(config: config)
8
+ def self.success(data)
9
+ new(data:)
10
10
  end
11
11
 
12
12
  def self.failure(type, message)
13
13
  new(error_type: type, error_message: message)
14
14
  end
15
15
 
16
- def initialize(config: nil, error_type: nil, error_message: nil)
17
- @config = config
16
+ def initialize(data: nil, error_type: nil, error_message: nil)
17
+ @data = data
18
18
  @error_type = error_type
19
19
  @error_message = error_message
20
20
  end
@@ -3,67 +3,90 @@
3
3
  module Nvoi
4
4
  module ConfigApi
5
5
  class << self
6
+ # Init (creates new config - special case, handles crypto)
7
+ def init(**args)
8
+ Actions::Init.new.call(**args)
9
+ end
10
+
11
+ # Domain Provider
12
+ def set_domain_provider(data, **args)
13
+ Actions::SetDomainProvider.new(data).call(**args)
14
+ end
15
+
16
+ def delete_domain_provider(data)
17
+ Actions::DeleteDomainProvider.new(data).call
18
+ end
19
+
6
20
  # Compute Provider
7
- def set_compute_provider(config, key, **args)
8
- Actions::SetComputeProvider.new(config, key).call(**args)
21
+ def set_compute_provider(data, **args)
22
+ Actions::SetComputeProvider.new(data).call(**args)
9
23
  end
10
24
 
11
- def delete_compute_provider(config, key)
12
- Actions::DeleteComputeProvider.new(config, key).call
25
+ def delete_compute_provider(data)
26
+ Actions::DeleteComputeProvider.new(data).call
13
27
  end
14
28
 
15
29
  # Server
16
- def set_server(config, key, **args)
17
- Actions::SetServer.new(config, key).call(**args)
30
+ def set_server(data, **args)
31
+ Actions::SetServer.new(data).call(**args)
18
32
  end
19
33
 
20
- def delete_server(config, key, **args)
21
- Actions::DeleteServer.new(config, key).call(**args)
34
+ def delete_server(data, **args)
35
+ Actions::DeleteServer.new(data).call(**args)
22
36
  end
23
37
 
24
38
  # Volume
25
- def set_volume(config, key, **args)
26
- Actions::SetVolume.new(config, key).call(**args)
39
+ def set_volume(data, **args)
40
+ Actions::SetVolume.new(data).call(**args)
27
41
  end
28
42
 
29
- def delete_volume(config, key, **args)
30
- Actions::DeleteVolume.new(config, key).call(**args)
43
+ def delete_volume(data, **args)
44
+ Actions::DeleteVolume.new(data).call(**args)
31
45
  end
32
46
 
33
47
  # App
34
- def set_app(config, key, **args)
35
- Actions::SetApp.new(config, key).call(**args)
48
+ def set_app(data, **args)
49
+ Actions::SetApp.new(data).call(**args)
36
50
  end
37
51
 
38
- def delete_app(config, key, **args)
39
- Actions::DeleteApp.new(config, key).call(**args)
52
+ def delete_app(data, **args)
53
+ Actions::DeleteApp.new(data).call(**args)
40
54
  end
41
55
 
42
56
  # Database
43
- def set_database(config, key, **args)
44
- Actions::SetDatabase.new(config, key).call(**args)
57
+ def set_database(data, **args)
58
+ Actions::SetDatabase.new(data).call(**args)
45
59
  end
46
60
 
47
- def delete_database(config, key)
48
- Actions::DeleteDatabase.new(config, key).call
61
+ def delete_database(data)
62
+ Actions::DeleteDatabase.new(data).call
49
63
  end
50
64
 
51
65
  # Secret
52
- def set_secret(config, key, **args)
53
- Actions::SetSecret.new(config, key).call(**args)
66
+ def set_secret(data, **args)
67
+ Actions::SetSecret.new(data).call(**args)
54
68
  end
55
69
 
56
- def delete_secret(config, key, **args)
57
- Actions::DeleteSecret.new(config, key).call(**args)
70
+ def delete_secret(data, **args)
71
+ Actions::DeleteSecret.new(data).call(**args)
58
72
  end
59
73
 
60
74
  # Env
61
- def set_env(config, key, **args)
62
- Actions::SetEnv.new(config, key).call(**args)
75
+ def set_env(data, **args)
76
+ Actions::SetEnv.new(data).call(**args)
77
+ end
78
+
79
+ def delete_env(data, **args)
80
+ Actions::DeleteEnv.new(data).call(**args)
81
+ end
82
+
83
+ # Service
84
+ def set_service(data, **args)
85
+ Actions::SetService.new(data).call(**args)
63
86
  end
64
87
 
65
- def delete_env(config, key, **args)
66
- Actions::DeleteEnv.new(config, key).call(**args)
88
+ def delete_service(data, **args)
89
+ Actions::DeleteService.new(data).call(**args)
67
90
  end
68
91
  end
69
92
  end
@@ -229,7 +229,7 @@ module Nvoi
229
229
  end
230
230
 
231
231
  def wait_for_server(server_id, max_attempts)
232
- server = Utils::Retry.poll(max_attempts: max_attempts, interval: 5) do
232
+ server = Utils::Retry.poll(max_attempts:, interval: 5) do
233
233
  resp = @client.describe_instances(instance_ids: [server_id])
234
234
 
235
235
  if resp.reservations.any? && resp.reservations[0].instances.any?
@@ -344,6 +344,31 @@ module Nvoi
344
344
  raise Errors::ValidationError, "aws credentials invalid: #{e.message}"
345
345
  end
346
346
 
347
+ # List available instance types for onboarding
348
+ def list_instance_types
349
+ # Common instance types (full list is huge)
350
+ common_types = %w[t3.micro t3.small t3.medium t3.large t3.xlarge m5.large m5.xlarge c5.large c5.xlarge]
351
+ resp = @client.describe_instance_types(instance_types: common_types)
352
+ resp.instance_types.map do |t|
353
+ {
354
+ name: t.instance_type,
355
+ vcpus: t.v_cpu_info.default_v_cpus,
356
+ memory: t.memory_info.size_in_mi_b
357
+ }
358
+ end
359
+ rescue StandardError
360
+ # Fallback to static list if API fails
361
+ common_types.map { |t| { name: t, vcpus: nil, memory: nil } }
362
+ end
363
+
364
+ # List available regions for onboarding
365
+ def list_regions
366
+ resp = @client.describe_regions
367
+ resp.regions.map do |r|
368
+ { name: r.region_name, endpoint: r.endpoint }
369
+ end
370
+ end
371
+
347
372
  private
348
373
 
349
374
  def find_vpc_by_name(name)
@@ -136,7 +136,7 @@ module Nvoi
136
136
  end
137
137
 
138
138
  def wait_for_server(server_id, max_attempts)
139
- server = Utils::Retry.poll(max_attempts: max_attempts, interval: Utils::Constants::SERVER_READY_INTERVAL) do
139
+ server = Utils::Retry.poll(max_attempts:, interval: Utils::Constants::SERVER_READY_INTERVAL) do
140
140
  s = get("/servers/#{server_id.to_i}")["server"]
141
141
  to_server(s) if s["status"] == "running"
142
142
  end
@@ -243,6 +243,32 @@ module Nvoi
243
243
  raise Errors::ValidationError, "hetzner credentials invalid: #{e.message}"
244
244
  end
245
245
 
246
+ # List available server types for onboarding
247
+ def list_server_types
248
+ get("/server_types")["server_types"].map do |t|
249
+ {
250
+ name: t["name"],
251
+ description: t["description"],
252
+ cores: t["cores"],
253
+ memory: t["memory"],
254
+ disk: t["disk"],
255
+ price: t.dig("prices", 0, "price_monthly", "gross")
256
+ }
257
+ end
258
+ end
259
+
260
+ # List available locations for onboarding
261
+ def list_locations
262
+ get("/locations")["locations"].map do |l|
263
+ {
264
+ name: l["name"],
265
+ city: l["city"],
266
+ country: l["country"],
267
+ description: l["description"]
268
+ }
269
+ end
270
+ end
271
+
246
272
  private
247
273
 
248
274
  def get(path)