nvoi 0.1.8 → 0.2.1

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 (122) 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/_TODO-rails-example.md +816 -0
  6. data/_TODO-rails-optimization.md +433 -0
  7. data/doc/config-schema.yaml +12 -0
  8. data/examples/apex-wildcard/deploy.yml +1 -0
  9. data/examples/golang-postgres-multi/deploy.yml +1 -0
  10. data/examples/postgres-multi/deploy.yml +1 -0
  11. data/examples/postgres-single/deploy.yml +1 -0
  12. data/examples/rails-single/deploy.yml +1 -0
  13. data/lib/nvoi/cli/config/command.rb +46 -41
  14. data/lib/nvoi/cli/credentials/edit/command.rb +24 -20
  15. data/lib/nvoi/cli/credentials/show/command.rb +1 -1
  16. data/lib/nvoi/cli/db/command.rb +10 -10
  17. data/lib/nvoi/cli/delete/command.rb +2 -2
  18. data/lib/nvoi/cli/deploy/command.rb +2 -2
  19. data/lib/nvoi/cli/deploy/steps/build_image.rb +2 -1
  20. data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +2 -2
  21. data/lib/nvoi/cli/deploy/steps/deploy_service.rb +7 -4
  22. data/lib/nvoi/cli/deploy/steps/provision_server.rb +1 -1
  23. data/lib/nvoi/cli/deploy/steps/provision_volume.rb +1 -1
  24. data/lib/nvoi/cli/exec/command.rb +3 -3
  25. data/lib/nvoi/cli/logs/command.rb +2 -2
  26. data/lib/nvoi/cli/onboard/command.rb +176 -622
  27. data/lib/nvoi/cli/onboard/steps/app.rb +108 -0
  28. data/lib/nvoi/cli/onboard/steps/app_name.rb +26 -0
  29. data/lib/nvoi/cli/onboard/steps/compute.rb +186 -0
  30. data/lib/nvoi/cli/onboard/steps/database.rb +97 -0
  31. data/lib/nvoi/cli/onboard/steps/domain.rb +48 -0
  32. data/lib/nvoi/cli/onboard/steps/env.rb +67 -0
  33. data/lib/nvoi/cli/onboard/ui.rb +84 -0
  34. data/lib/nvoi/cli/unlock/command.rb +2 -2
  35. data/lib/nvoi/cli.rb +2 -33
  36. data/lib/nvoi/configuration/app_service.rb +54 -0
  37. data/lib/nvoi/configuration/application.rb +44 -0
  38. data/lib/nvoi/configuration/builder.rb +420 -0
  39. data/lib/nvoi/configuration/database.rb +56 -0
  40. data/lib/nvoi/configuration/deploy.rb +15 -0
  41. data/lib/nvoi/{objects/service_spec.rb → configuration/deployment.rb} +4 -3
  42. data/lib/nvoi/{objects/config_override.rb → configuration/override.rb} +4 -4
  43. data/lib/nvoi/configuration/providers.rb +81 -0
  44. data/lib/nvoi/configuration/result.rb +43 -0
  45. data/lib/nvoi/configuration/root.rb +252 -0
  46. data/lib/nvoi/configuration/server.rb +39 -0
  47. data/lib/nvoi/configuration/service.rb +51 -0
  48. data/lib/nvoi/configuration/ssh_key.rb +16 -0
  49. data/lib/nvoi/external/cloud/aws.rb +26 -16
  50. data/lib/nvoi/external/cloud/hetzner.rb +40 -25
  51. data/lib/nvoi/external/cloud/scaleway.rb +10 -8
  52. data/lib/nvoi/external/cloud/types.rb +42 -0
  53. data/lib/nvoi/external/database/mysql.rb +1 -1
  54. data/lib/nvoi/external/database/postgres.rb +1 -1
  55. data/lib/nvoi/external/database/provider.rb +1 -1
  56. data/lib/nvoi/external/database/sqlite.rb +1 -1
  57. data/lib/nvoi/external/database/types.rb +55 -0
  58. data/lib/nvoi/external/dns/cloudflare.rb +11 -11
  59. data/lib/nvoi/external/dns/types.rb +24 -0
  60. data/lib/nvoi/utils/config_loader.rb +12 -12
  61. data/lib/nvoi/utils/credential_store.rb +4 -4
  62. data/lib/nvoi/utils/env_resolver.rb +3 -3
  63. data/lib/nvoi/utils/namer.rb +8 -3
  64. data/lib/nvoi/utils/presence.rb +23 -0
  65. data/lib/nvoi/version.rb +1 -1
  66. data/lib/nvoi.rb +2 -17
  67. metadata +98 -59
  68. data/.claude/todo/refactor/00-overview.md +0 -171
  69. data/.claude/todo/refactor/01-objects.md +0 -96
  70. data/.claude/todo/refactor/02-utils.md +0 -143
  71. data/.claude/todo/refactor/03-external-cloud.md +0 -164
  72. data/.claude/todo/refactor/04-external-dns.md +0 -104
  73. data/.claude/todo/refactor/05-external.md +0 -133
  74. data/.claude/todo/refactor/06-cli.md +0 -123
  75. data/.claude/todo/refactor/07-cli-deploy-command.md +0 -177
  76. data/.claude/todo/refactor/08-cli-deploy-steps.md +0 -201
  77. data/.claude/todo/refactor/09-cli-delete-command.md +0 -169
  78. data/.claude/todo/refactor/10-cli-exec-command.md +0 -157
  79. data/.claude/todo/refactor/11-cli-credentials-command.md +0 -190
  80. data/.claude/todo/refactor/12-cli-db-command.md +0 -128
  81. data/.claude/todo/refactor/_target.md +0 -79
  82. data/.claude/todo/refactor-execution/00-entrypoint.md +0 -49
  83. data/.claude/todo/refactor-execution/01-objects.md +0 -42
  84. data/.claude/todo/refactor-execution/02-utils.md +0 -41
  85. data/.claude/todo/refactor-execution/03-external-cloud.md +0 -38
  86. data/.claude/todo/refactor-execution/04-external-dns.md +0 -35
  87. data/.claude/todo/refactor-execution/05-external-other.md +0 -46
  88. data/.claude/todo/refactor-execution/06-cli-deploy.md +0 -45
  89. data/.claude/todo/refactor-execution/07-cli-delete.md +0 -43
  90. data/.claude/todo/refactor-execution/08-cli-exec.md +0 -30
  91. data/.claude/todo/refactor-execution/09-cli-credentials.md +0 -34
  92. data/.claude/todo/refactor-execution/10-cli-db.md +0 -31
  93. data/.claude/todo/refactor-execution/11-cli-router.md +0 -44
  94. data/.claude/todo/refactor-execution/12-cleanup.md +0 -120
  95. data/.claude/todo/refactor-execution/_monitoring-strategy.md +0 -126
  96. data/.claude/todo/scaleway.impl.md +0 -644
  97. data/.claude/todo/scaleway.reference.md +0 -520
  98. data/.claude/todos/buckets.md +0 -41
  99. data/.claude/todos.md +0 -550
  100. data/Makefile +0 -26
  101. data/ingest +0 -0
  102. data/lib/nvoi/config_api/actions/app.rb +0 -53
  103. data/lib/nvoi/config_api/actions/compute_provider.rb +0 -55
  104. data/lib/nvoi/config_api/actions/database.rb +0 -70
  105. data/lib/nvoi/config_api/actions/domain_provider.rb +0 -40
  106. data/lib/nvoi/config_api/actions/env.rb +0 -32
  107. data/lib/nvoi/config_api/actions/init.rb +0 -67
  108. data/lib/nvoi/config_api/actions/secret.rb +0 -32
  109. data/lib/nvoi/config_api/actions/server.rb +0 -66
  110. data/lib/nvoi/config_api/actions/service.rb +0 -52
  111. data/lib/nvoi/config_api/actions/volume.rb +0 -40
  112. data/lib/nvoi/config_api/base.rb +0 -38
  113. data/lib/nvoi/config_api/result.rb +0 -26
  114. data/lib/nvoi/config_api.rb +0 -93
  115. data/lib/nvoi/objects/configuration.rb +0 -483
  116. data/lib/nvoi/objects/database.rb +0 -56
  117. data/lib/nvoi/objects/dns.rb +0 -14
  118. data/lib/nvoi/objects/firewall.rb +0 -11
  119. data/lib/nvoi/objects/network.rb +0 -11
  120. data/lib/nvoi/objects/server.rb +0 -14
  121. data/lib/nvoi/objects/tunnel.rb +0 -14
  122. data/lib/nvoi/objects/volume.rb +0 -17
@@ -146,14 +146,14 @@ module Nvoi
146
146
  }
147
147
 
148
148
  # Add security group if provided
149
- if opts.firewall_id && !opts.firewall_id.empty?
149
+ unless opts.firewall_id.blank?
150
150
  create_opts[:security_group] = opts.firewall_id
151
151
  end
152
152
 
153
153
  server = post(instance_url("/servers"), create_opts)["server"]
154
154
 
155
155
  # Set cloud-init user data if provided
156
- if opts.user_data && !opts.user_data.empty?
156
+ unless opts.user_data.blank?
157
157
  set_user_data(server["id"], "cloud-init", opts.user_data)
158
158
  end
159
159
 
@@ -161,7 +161,7 @@ module Nvoi
161
161
  server_action(server["id"], "poweron")
162
162
 
163
163
  # Attach to private network if provided
164
- if opts.network_id && !opts.network_id.empty?
164
+ unless opts.network_id.blank?
165
165
  wait_for_server_state(server["id"], "running", 30)
166
166
  create_private_nic(server["id"], opts.network_id)
167
167
  end
@@ -313,11 +313,13 @@ module Nvoi
313
313
  # List available server types for onboarding
314
314
  def list_server_types
315
315
  list_server_types_api.map do |name, info|
316
+ arch = info.dig("arch") || "x86_64"
316
317
  {
317
318
  name:,
318
319
  cores: info.dig("ncpus"),
319
320
  ram: info.dig("ram"),
320
- hourly_price: info.dig("hourly_price")
321
+ hourly_price: info.dig("hourly_price"),
322
+ architecture: arch.include?("arm") ? "arm64" : "x86"
321
323
  }
322
324
  end
323
325
  end
@@ -511,7 +513,7 @@ module Nvoi
511
513
  end
512
514
 
513
515
  def to_network(data)
514
- Objects::Network::Record.new(
516
+ Types::Network::Record.new(
515
517
  id: data["id"],
516
518
  name: data["name"],
517
519
  ip_range: data.dig("subnets", 0, "subnet") || data["subnets"]&.first
@@ -519,7 +521,7 @@ module Nvoi
519
521
  end
520
522
 
521
523
  def to_firewall(data)
522
- Objects::Firewall::Record.new(
524
+ Types::Firewall::Record.new(
523
525
  id: data["id"],
524
526
  name: data["name"]
525
527
  )
@@ -529,7 +531,7 @@ module Nvoi
529
531
  # Scaleway doesn't include private_ips in the NIC response directly
530
532
  # We'd need to call IPAM API which adds complexity
531
533
  # Instead, private IP discovery happens via SSH in setup_k3s
532
- Objects::Server::Record.new(
534
+ Types::Server::Record.new(
533
535
  id: data["id"],
534
536
  name: data["name"],
535
537
  status: data["state"],
@@ -543,7 +545,7 @@ module Nvoi
543
545
  r["product_resource_type"] == "instance_server"
544
546
  }&.dig("product_resource_id")
545
547
 
546
- Objects::Volume::Record.new(
548
+ Types::Volume::Record.new(
547
549
  id: data["id"],
548
550
  name: data["name"],
549
551
  size: (data["size"] || 0) / 1_000_000_000,
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module External
5
+ module Cloud
6
+ module Types
7
+ # Server-related structs
8
+ module Server
9
+ # Record represents a compute server/instance
10
+ Record = Struct.new(:id, :name, :status, :public_ipv4, :private_ipv4, keyword_init: true)
11
+
12
+ # CreateOptions contains options for creating a server
13
+ CreateOptions = Struct.new(:name, :type, :image, :location, :user_data, :network_id, :firewall_id, :ssh_keys, keyword_init: true)
14
+ end
15
+
16
+ # Volume-related structs
17
+ module Volume
18
+ # Record represents a block storage volume
19
+ Record = Struct.new(:id, :name, :size, :location, :status, :server_id, :device_path, keyword_init: true)
20
+
21
+ # CreateOptions contains options for creating a volume
22
+ CreateOptions = Struct.new(:name, :size, :server_id, :location, keyword_init: true)
23
+
24
+ # MountOptions contains options for mounting a volume
25
+ MountOptions = Struct.new(:device_path, :mount_path, :fs_type, keyword_init: true)
26
+ end
27
+
28
+ # Network-related structs
29
+ module Network
30
+ # Record represents a virtual network
31
+ Record = Struct.new(:id, :name, :ip_range, keyword_init: true)
32
+ end
33
+
34
+ # Firewall-related structs
35
+ module Firewall
36
+ # Record represents a firewall configuration
37
+ Record = Struct.new(:id, :name, keyword_init: true)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -48,7 +48,7 @@ module Nvoi
48
48
  end
49
49
 
50
50
  def restore(ssh, data, opts)
51
- create_database(ssh, Objects::Database::CreateOptions.new(
51
+ create_database(ssh, Types::CreateOptions.new(
52
52
  pod_name: opts.pod_name,
53
53
  database: opts.database,
54
54
  user: opts.user,
@@ -46,7 +46,7 @@ module Nvoi
46
46
  end
47
47
 
48
48
  def restore(ssh, data, opts)
49
- create_database(ssh, Objects::Database::CreateOptions.new(
49
+ create_database(ssh, Types::CreateOptions.new(
50
50
  pod_name: opts.pod_name,
51
51
  database: opts.database,
52
52
  user: opts.user,
@@ -49,7 +49,7 @@ module Nvoi
49
49
 
50
50
  def parse_standard_url(url, default_port)
51
51
  uri = URI.parse(url)
52
- Objects::Database::Credentials.new(
52
+ Types::Credentials.new(
53
53
  user: uri.user,
54
54
  password: uri.password,
55
55
  host: uri.host,
@@ -15,7 +15,7 @@ module Nvoi
15
15
 
16
16
  def parse_url(url)
17
17
  path = url.sub(%r{^sqlite3?:///?}, "")
18
- Objects::Database::Credentials.new(
18
+ Types::Credentials.new(
19
19
  path:,
20
20
  database: File.basename(path)
21
21
  )
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module External
5
+ module Database
6
+ module Types
7
+ # Parsed credentials from database URL
8
+ Credentials = Struct.new(:user, :password, :host, :port, :database, :path, :host_path, keyword_init: true)
9
+
10
+ # Options for dumping a database
11
+ DumpOptions = Struct.new(:pod_name, :database, :user, :password, :host_path, keyword_init: true)
12
+
13
+ # Options for restoring a database
14
+ RestoreOptions = Struct.new(:pod_name, :database, :user, :password, :source_db, :host_path, keyword_init: true)
15
+
16
+ # Options for creating a database
17
+ CreateOptions = Struct.new(:pod_name, :database, :user, :password, keyword_init: true)
18
+
19
+ # Branch represents a database branch (snapshot)
20
+ Branch = Struct.new(:id, :created_at, :size, :adapter, :database, keyword_init: true) do
21
+ def to_h
22
+ { id:, created_at:, size:, adapter:, database: }
23
+ end
24
+ end
25
+
26
+ # BranchMetadata holds all branches for an app
27
+ class BranchMetadata
28
+ attr_accessor :branches
29
+
30
+ def initialize(branches = [])
31
+ @branches = branches
32
+ end
33
+
34
+ def to_json(*_args)
35
+ JSON.pretty_generate({ branches: @branches.map(&:to_h) })
36
+ end
37
+
38
+ def self.from_json(json_str)
39
+ data = JSON.parse(json_str)
40
+ branches = (data["branches"] || []).map do |b|
41
+ Branch.new(
42
+ id: b["id"],
43
+ created_at: b["created_at"],
44
+ size: b["size"],
45
+ adapter: b["adapter"],
46
+ database: b["database"]
47
+ )
48
+ end
49
+ new(branches)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -36,7 +36,7 @@ module Nvoi
36
36
  })
37
37
 
38
38
  result = response["result"]
39
- Objects::Tunnel::Record.new(
39
+ Types::Tunnel::Record.new(
40
40
  id: result["id"],
41
41
  name: result["name"],
42
42
  token: result["token"]
@@ -51,7 +51,7 @@ module Nvoi
51
51
  return nil if results.nil? || results.empty?
52
52
 
53
53
  result = results[0]
54
- Objects::Tunnel::Record.new(
54
+ Types::Tunnel::Record.new(
55
55
  id: result["id"],
56
56
  name: result["name"],
57
57
  token: result["token"]
@@ -69,11 +69,11 @@ module Nvoi
69
69
  url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}/configurations"
70
70
 
71
71
  ingress_rules = hostnames.map do |hostname|
72
- {
73
- hostname:,
74
- service: service_url,
75
- originRequest: { httpHostHeader: hostname.sub(/^\*\./, "") } # Use apex for wildcard
76
- }
72
+ rule = { hostname:, service: service_url }
73
+ # Only set httpHostHeader for non-wildcard hostnames
74
+ # Wildcards should pass through the original Host header
75
+ rule[:originRequest] = { httpHostHeader: hostname } unless hostname.start_with?("*.")
76
+ rule
77
77
  end
78
78
  ingress_rules << { service: "http_status:404" }
79
79
 
@@ -144,7 +144,7 @@ module Nvoi
144
144
  zone_data = results.find { |z| z["name"] == domain }
145
145
  return nil unless zone_data
146
146
 
147
- Objects::Dns::Zone.new(id: zone_data["id"], name: zone_data["name"])
147
+ Types::Zone.new(id: zone_data["id"], name: zone_data["name"])
148
148
  end
149
149
 
150
150
  def subdomain_available?(zone_id, subdomain, domain)
@@ -163,7 +163,7 @@ module Nvoi
163
163
  record_data = results.find { |r| r["name"] == name && r["type"] == record_type }
164
164
  return nil unless record_data
165
165
 
166
- Objects::Dns::Record.new(
166
+ Types::Record.new(
167
167
  id: record_data["id"],
168
168
  type: record_data["type"],
169
169
  name: record_data["name"],
@@ -185,7 +185,7 @@ module Nvoi
185
185
  })
186
186
 
187
187
  result = response["result"]
188
- Objects::Dns::Record.new(
188
+ Types::Record.new(
189
189
  id: result["id"],
190
190
  type: result["type"],
191
191
  name: result["name"],
@@ -207,7 +207,7 @@ module Nvoi
207
207
  })
208
208
 
209
209
  result = response["result"]
210
- Objects::Dns::Record.new(
210
+ Types::Record.new(
211
211
  id: result["id"],
212
212
  type: result["type"],
213
213
  name: result["name"],
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module External
5
+ module Dns
6
+ module Types
7
+ # Zone represents a Cloudflare DNS zone
8
+ Zone = Struct.new(:id, :name, keyword_init: true)
9
+
10
+ # Record represents a Cloudflare DNS record
11
+ Record = Struct.new(:id, :type, :name, :content, :proxied, :ttl, keyword_init: true)
12
+
13
+ # Tunnel-related structs (Cloudflare tunnels)
14
+ module Tunnel
15
+ # Record represents a Cloudflare tunnel
16
+ Record = Struct.new(:id, :name, :token, keyword_init: true)
17
+
18
+ # Info holds information about a configured tunnel
19
+ Info = Struct.new(:service_name, :hostname, :tunnel_id, :tunnel_token, :port, keyword_init: true)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -10,8 +10,8 @@ module Nvoi
10
10
  class << self
11
11
  # Load reads and parses the deployment configuration from encrypted file
12
12
  def load(config_path, credentials_path: nil, master_key_path: nil)
13
- working_dir = config_path && !config_path.empty? ? File.dirname(config_path) : "."
14
- enc_path = credentials_path.nil? || credentials_path.empty? ? config_path : credentials_path
13
+ working_dir = config_path.blank? ? "." : File.dirname(config_path)
14
+ enc_path = credentials_path.blank? ? config_path : credentials_path
15
15
 
16
16
  manager = CredentialStore.new(working_dir, enc_path, master_key_path)
17
17
  plaintext = manager.read
@@ -20,8 +20,8 @@ module Nvoi
20
20
  data = YAML.safe_load(plaintext, permitted_classes: [Symbol])
21
21
  raise Errors::ConfigError, "Invalid config format" unless data.is_a?(Hash)
22
22
 
23
- deploy_config = Objects::Configuration::Deploy.new(data)
24
- cfg = Objects::Configuration::Root.new(deploy_config)
23
+ deploy_config = Configuration::Deploy.new(data)
24
+ cfg = Configuration::Root.new(deploy_config)
25
25
 
26
26
  load_ssh_keys(cfg)
27
27
  cfg.validate_config
@@ -39,7 +39,7 @@ module Nvoi
39
39
 
40
40
  provider = External::Database.provider_for(adapter)
41
41
 
42
- if db_config.url && !db_config.url.empty?
42
+ unless db_config.url.blank?
43
43
  creds = provider.parse_url(db_config.url)
44
44
  host_path = nil
45
45
 
@@ -47,7 +47,7 @@ module Nvoi
47
47
  host_path = resolve_sqlite_host_path(db_config, namer, creds.database || "app.db")
48
48
  end
49
49
 
50
- return Objects::Database::Credentials.new(
50
+ return External::Database::Types::Credentials.new(
51
51
  user: creds.user,
52
52
  password: creds.password,
53
53
  host: creds.host,
@@ -61,21 +61,21 @@ module Nvoi
61
61
  # Fall back to secrets-based credentials
62
62
  case adapter
63
63
  when "postgres", "postgresql"
64
- Objects::Database::Credentials.new(
64
+ External::Database::Types::Credentials.new(
65
65
  port: provider.default_port,
66
66
  user: db_config.secrets["POSTGRES_USER"],
67
67
  password: db_config.secrets["POSTGRES_PASSWORD"],
68
68
  database: db_config.secrets["POSTGRES_DB"]
69
69
  )
70
70
  when "mysql", "mysql2"
71
- Objects::Database::Credentials.new(
71
+ External::Database::Types::Credentials.new(
72
72
  port: provider.default_port,
73
73
  user: db_config.secrets["MYSQL_USER"],
74
74
  password: db_config.secrets["MYSQL_PASSWORD"],
75
75
  database: db_config.secrets["MYSQL_DATABASE"]
76
76
  )
77
77
  when "sqlite", "sqlite3"
78
- Objects::Database::Credentials.new(
78
+ External::Database::Types::Credentials.new(
79
79
  database: "app.db",
80
80
  host_path: resolve_sqlite_host_path(db_config, namer, "app.db")
81
81
  )
@@ -93,8 +93,8 @@ module Nvoi
93
93
  raise Errors::ConfigError, "ssh_keys section is required in config. Run 'nvoi credentials edit' to generate keys."
94
94
  end
95
95
 
96
- raise Errors::ConfigError, "ssh_keys.private_key is required" unless ssh_keys.private_key && !ssh_keys.private_key.empty?
97
- raise Errors::ConfigError, "ssh_keys.public_key is required" unless ssh_keys.public_key && !ssh_keys.public_key.empty?
96
+ raise Errors::ConfigError, "ssh_keys.private_key is required" if ssh_keys.private_key.blank?
97
+ raise Errors::ConfigError, "ssh_keys.public_key is required" if ssh_keys.public_key.blank?
98
98
 
99
99
  temp_dir = Dir.mktmpdir("nvoi-ssh-")
100
100
 
@@ -136,7 +136,7 @@ module Nvoi
136
136
  server_name = db_config.servers.first
137
137
  mount = db_config.mount
138
138
 
139
- if mount && !mount.empty?
139
+ unless mount.blank?
140
140
  vol_name = mount.keys.first
141
141
  base_path = namer.server_volume_host_path(server_name, vol_name)
142
142
  return "#{base_path}/#{filename}"
@@ -17,7 +17,7 @@ module Nvoi
17
17
  # key_path: explicit path to key file (optional, nil = auto-discover)
18
18
  def initialize(working_dir, encrypted_path = nil, key_path = nil)
19
19
  @working_dir = working_dir
20
- @encrypted_path = encrypted_path && !encrypted_path.empty? ? encrypted_path : find_encrypted_file
20
+ @encrypted_path = encrypted_path.blank? ? find_encrypted_file : encrypted_path
21
21
  @key_path = nil
22
22
  @master_key = nil
23
23
 
@@ -41,7 +41,7 @@ module Nvoi
41
41
 
42
42
  # Check if the store has a master key loaded
43
43
  def has_key?
44
- !@master_key.nil? && !@master_key.empty?
44
+ !@master_key.blank?
45
45
  end
46
46
 
47
47
  # Decrypt and return the credentials content
@@ -131,7 +131,7 @@ module Nvoi
131
131
 
132
132
  def resolve_key(explicit_key_path)
133
133
  # Priority 1: Explicit key file path
134
- if explicit_key_path && !explicit_key_path.empty?
134
+ unless explicit_key_path.blank?
135
135
  @master_key = load_key_from_file(explicit_key_path)
136
136
  @key_path = explicit_key_path
137
137
  return
@@ -139,7 +139,7 @@ module Nvoi
139
139
 
140
140
  # Priority 2: Environment variable
141
141
  env_key = ENV[MASTER_KEY_ENV_VAR]
142
- if env_key && !env_key.empty?
142
+ unless env_key.blank?
143
143
  Crypto.validate_key(env_key)
144
144
  @master_key = env_key
145
145
  return
@@ -44,12 +44,12 @@ module Nvoi
44
44
  db = @config.deploy.application.database
45
45
  return unless db
46
46
 
47
- env["DATABASE_ADAPTER"] = db.adapter if db.adapter && !db.adapter.empty?
47
+ env["DATABASE_ADAPTER"] = db.adapter unless db.adapter.blank?
48
48
 
49
49
  # Handle database URL
50
50
  if db.adapter == "sqlite3"
51
51
  env["DATABASE_URL"] = sqlite_database_url(db)
52
- elsif db.url && !db.url.empty?
52
+ elsif !db.url.blank?
53
53
  env["DATABASE_URL"] = db.url
54
54
  end
55
55
 
@@ -60,7 +60,7 @@ module Nvoi
60
60
  end
61
61
 
62
62
  def sqlite_database_url(db)
63
- raise Errors::ConfigError, "sqlite3 requires database.mount to be configured" if db.mount.nil? || db.mount.empty?
63
+ raise Errors::ConfigError, "sqlite3 requires database.mount to be configured" if db.mount.blank?
64
64
 
65
65
  mount_path = db.mount.values.first
66
66
  app_name = @config.deploy.application.name
@@ -33,7 +33,7 @@ module Nvoi
33
33
 
34
34
  # ServerName returns the server name for a given group and index
35
35
  def server_name(group, index)
36
- "#{@config.deploy.application.name}-#{group}-#{index}"
36
+ "#{sanitize_name(@config.deploy.application.name)}-#{group}-#{index}"
37
37
  end
38
38
 
39
39
  def firewall_name
@@ -178,7 +178,7 @@ module Nvoi
178
178
 
179
179
  # Class method for building hostname without instance
180
180
  def self.build_hostname(subdomain, domain)
181
- if subdomain.nil? || subdomain.empty? || subdomain == "@"
181
+ if subdomain.blank? || subdomain == "@"
182
182
  domain
183
183
  else
184
184
  "#{subdomain}.#{domain}"
@@ -187,7 +187,7 @@ module Nvoi
187
187
 
188
188
  # Returns array of hostnames - apex returns [domain, *.domain], subdomain returns [sub.domain]
189
189
  def self.build_hostnames(subdomain, domain)
190
- if subdomain.nil? || subdomain.empty? || subdomain == "@"
190
+ if subdomain.blank? || subdomain == "@"
191
191
  [domain, "*.#{domain}"]
192
192
  else
193
193
  ["#{subdomain}.#{domain}"]
@@ -196,6 +196,11 @@ module Nvoi
196
196
 
197
197
  private
198
198
 
199
+ # Sanitize name for cloud provider compatibility (no underscores, lowercase, etc.)
200
+ def sanitize_name(name)
201
+ name.to_s.gsub("_", "-").downcase
202
+ end
203
+
199
204
  def hash_string(str)
200
205
  Digest::SHA256.hexdigest(str)[0, 16]
201
206
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Core extensions for blank?/present? checks
4
+
5
+ class NilClass
6
+ def blank? = true
7
+ def present? = false
8
+ end
9
+
10
+ class String
11
+ def blank? = empty?
12
+ def present? = !empty?
13
+ end
14
+
15
+ class Array
16
+ def blank? = empty?
17
+ def present? = !empty?
18
+ end
19
+
20
+ class Hash
21
+ def blank? = empty?
22
+ def present? = !empty?
23
+ end
data/lib/nvoi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nvoi
4
- VERSION = "0.1.8"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/nvoi.rb CHANGED
@@ -13,26 +13,11 @@ require "fileutils"
13
13
  require "tempfile"
14
14
  require "open3"
15
15
  require "faraday"
16
+ require_relative "nvoi/utils/presence"
16
17
 
17
18
  loader = Zeitwerk::Loader.for_gem
18
- loader.ignore("#{__dir__}/nvoi/cli") # CLI commands are lazy-loaded
19
- loader.ignore("#{__dir__}/nvoi/config_api") # ConfigApi uses non-standard naming
20
19
  loader.setup
21
-
22
- # Load ConfigApi manually (uses non-standard naming convention)
23
- require_relative "nvoi/config_api/result"
24
- require_relative "nvoi/config_api/base"
25
- require_relative "nvoi/config_api/actions/init"
26
- require_relative "nvoi/config_api/actions/domain_provider"
27
- require_relative "nvoi/config_api/actions/compute_provider"
28
- require_relative "nvoi/config_api/actions/server"
29
- require_relative "nvoi/config_api/actions/volume"
30
- require_relative "nvoi/config_api/actions/app"
31
- require_relative "nvoi/config_api/actions/database"
32
- require_relative "nvoi/config_api/actions/secret"
33
- require_relative "nvoi/config_api/actions/env"
34
- require_relative "nvoi/config_api/actions/service"
35
- require_relative "nvoi/config_api"
20
+ loader.eager_load_namespace(Nvoi::Cli)
36
21
 
37
22
  module Nvoi
38
23
  class << self