nvoi 0.1.7 → 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 (114) 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 +29 -13
  11. data/lib/nvoi/cli/deploy/steps/build_image.rb +48 -6
  12. data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +2 -2
  13. data/lib/nvoi/cli/deploy/steps/deploy_service.rb +3 -13
  14. data/lib/nvoi/cli/deploy/steps/provision_server.rb +1 -1
  15. data/lib/nvoi/cli/deploy/steps/provision_volume.rb +1 -1
  16. data/lib/nvoi/cli/exec/command.rb +3 -3
  17. data/lib/nvoi/cli/logs/command.rb +2 -2
  18. data/lib/nvoi/cli/onboard/command.rb +176 -622
  19. data/lib/nvoi/cli/onboard/steps/app.rb +108 -0
  20. data/lib/nvoi/cli/onboard/steps/app_name.rb +26 -0
  21. data/lib/nvoi/cli/onboard/steps/compute.rb +139 -0
  22. data/lib/nvoi/cli/onboard/steps/database.rb +97 -0
  23. data/lib/nvoi/cli/onboard/steps/domain.rb +48 -0
  24. data/lib/nvoi/cli/onboard/steps/env.rb +67 -0
  25. data/lib/nvoi/cli/onboard/ui.rb +84 -0
  26. data/lib/nvoi/cli/unlock/command.rb +2 -2
  27. data/lib/nvoi/cli.rb +0 -32
  28. data/lib/nvoi/configuration/app_service.rb +54 -0
  29. data/lib/nvoi/configuration/application.rb +44 -0
  30. data/lib/nvoi/configuration/builder.rb +417 -0
  31. data/lib/nvoi/configuration/database.rb +56 -0
  32. data/lib/nvoi/configuration/deploy.rb +15 -0
  33. data/lib/nvoi/{objects/service_spec.rb → configuration/deployment.rb} +4 -3
  34. data/lib/nvoi/{objects/config_override.rb → configuration/override.rb} +4 -4
  35. data/lib/nvoi/configuration/providers.rb +78 -0
  36. data/lib/nvoi/configuration/result.rb +43 -0
  37. data/lib/nvoi/configuration/root.rb +234 -0
  38. data/lib/nvoi/configuration/server.rb +39 -0
  39. data/lib/nvoi/configuration/service.rb +62 -0
  40. data/lib/nvoi/external/cloud/aws.rb +12 -12
  41. data/lib/nvoi/external/cloud/hetzner.rb +7 -7
  42. data/lib/nvoi/external/cloud/scaleway.rb +7 -7
  43. data/lib/nvoi/external/cloud/types.rb +42 -0
  44. data/lib/nvoi/external/containerd.rb +1 -48
  45. data/lib/nvoi/external/database/mysql.rb +1 -1
  46. data/lib/nvoi/external/database/postgres.rb +1 -1
  47. data/lib/nvoi/external/database/provider.rb +1 -1
  48. data/lib/nvoi/external/database/sqlite.rb +1 -1
  49. data/lib/nvoi/external/database/types.rb +55 -0
  50. data/lib/nvoi/external/dns/cloudflare.rb +6 -6
  51. data/lib/nvoi/external/dns/types.rb +24 -0
  52. data/lib/nvoi/external/ssh.rb +0 -12
  53. data/lib/nvoi/external/ssh_tunnel.rb +100 -0
  54. data/lib/nvoi/utils/config_loader.rb +12 -12
  55. data/lib/nvoi/utils/credential_store.rb +4 -4
  56. data/lib/nvoi/utils/env_resolver.rb +3 -3
  57. data/lib/nvoi/utils/namer.rb +2 -2
  58. data/lib/nvoi/utils/presence.rb +23 -0
  59. data/lib/nvoi/version.rb +1 -1
  60. data/lib/nvoi.rb +2 -17
  61. metadata +96 -57
  62. data/.claude/todo/refactor/00-overview.md +0 -171
  63. data/.claude/todo/refactor/01-objects.md +0 -96
  64. data/.claude/todo/refactor/02-utils.md +0 -143
  65. data/.claude/todo/refactor/03-external-cloud.md +0 -164
  66. data/.claude/todo/refactor/04-external-dns.md +0 -104
  67. data/.claude/todo/refactor/05-external.md +0 -133
  68. data/.claude/todo/refactor/06-cli.md +0 -123
  69. data/.claude/todo/refactor/07-cli-deploy-command.md +0 -177
  70. data/.claude/todo/refactor/08-cli-deploy-steps.md +0 -201
  71. data/.claude/todo/refactor/09-cli-delete-command.md +0 -169
  72. data/.claude/todo/refactor/10-cli-exec-command.md +0 -157
  73. data/.claude/todo/refactor/11-cli-credentials-command.md +0 -190
  74. data/.claude/todo/refactor/12-cli-db-command.md +0 -128
  75. data/.claude/todo/refactor/_target.md +0 -79
  76. data/.claude/todo/refactor-execution/00-entrypoint.md +0 -49
  77. data/.claude/todo/refactor-execution/01-objects.md +0 -42
  78. data/.claude/todo/refactor-execution/02-utils.md +0 -41
  79. data/.claude/todo/refactor-execution/03-external-cloud.md +0 -38
  80. data/.claude/todo/refactor-execution/04-external-dns.md +0 -35
  81. data/.claude/todo/refactor-execution/05-external-other.md +0 -46
  82. data/.claude/todo/refactor-execution/06-cli-deploy.md +0 -45
  83. data/.claude/todo/refactor-execution/07-cli-delete.md +0 -43
  84. data/.claude/todo/refactor-execution/08-cli-exec.md +0 -30
  85. data/.claude/todo/refactor-execution/09-cli-credentials.md +0 -34
  86. data/.claude/todo/refactor-execution/10-cli-db.md +0 -31
  87. data/.claude/todo/refactor-execution/11-cli-router.md +0 -44
  88. data/.claude/todo/refactor-execution/12-cleanup.md +0 -120
  89. data/.claude/todo/refactor-execution/_monitoring-strategy.md +0 -126
  90. data/.claude/todo/scaleway.impl.md +0 -644
  91. data/.claude/todo/scaleway.reference.md +0 -520
  92. data/.claude/todos.md +0 -550
  93. data/ingest +0 -0
  94. data/lib/nvoi/config_api/actions/app.rb +0 -53
  95. data/lib/nvoi/config_api/actions/compute_provider.rb +0 -55
  96. data/lib/nvoi/config_api/actions/database.rb +0 -70
  97. data/lib/nvoi/config_api/actions/domain_provider.rb +0 -40
  98. data/lib/nvoi/config_api/actions/env.rb +0 -32
  99. data/lib/nvoi/config_api/actions/init.rb +0 -67
  100. data/lib/nvoi/config_api/actions/secret.rb +0 -32
  101. data/lib/nvoi/config_api/actions/server.rb +0 -66
  102. data/lib/nvoi/config_api/actions/service.rb +0 -52
  103. data/lib/nvoi/config_api/actions/volume.rb +0 -40
  104. data/lib/nvoi/config_api/base.rb +0 -38
  105. data/lib/nvoi/config_api/result.rb +0 -26
  106. data/lib/nvoi/config_api.rb +0 -93
  107. data/lib/nvoi/objects/configuration.rb +0 -483
  108. data/lib/nvoi/objects/database.rb +0 -56
  109. data/lib/nvoi/objects/dns.rb +0 -14
  110. data/lib/nvoi/objects/firewall.rb +0 -11
  111. data/lib/nvoi/objects/network.rb +0 -11
  112. data/lib/nvoi/objects/server.rb +0 -14
  113. data/lib/nvoi/objects/tunnel.rb +0 -14
  114. data/lib/nvoi/objects/volume.rb +0 -17
@@ -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"]
@@ -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
@@ -62,18 +62,6 @@ module Nvoi
62
62
  raise Errors::SshCommandError, "SCP download failed: #{output}" unless status.success?
63
63
  end
64
64
 
65
- def rsync(local_path, remote_path)
66
- rsync_args = [
67
- "-avz",
68
- "-e", "ssh #{build_ssh_args.join(' ')}",
69
- local_path,
70
- "#{@user}@#{@ip}:#{remote_path}"
71
- ]
72
-
73
- output, status = Open3.capture2e("rsync", *rsync_args)
74
- raise Errors::SshCommandError, "rsync failed: #{output}" unless status.success?
75
- end
76
-
77
65
  private
78
66
 
79
67
  def build_ssh_args
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/ssh"
4
+
5
+ module Nvoi
6
+ module External
7
+ # SshTunnel manages SSH port forwarding using net-ssh
8
+ class SshTunnel
9
+ attr_reader :local_port, :remote_port
10
+
11
+ def initialize(ip:, user:, key_path:, local_port:, remote_port:)
12
+ @ip = ip
13
+ @user = user
14
+ @key_path = key_path
15
+ @local_port = local_port
16
+ @remote_port = remote_port
17
+ @session = nil
18
+ @thread = nil
19
+ @running = false
20
+ end
21
+
22
+ def start
23
+ Nvoi.logger.info "Starting SSH tunnel: localhost:%d -> %s:%d", @local_port, @ip, @remote_port
24
+
25
+ @session = Net::SSH.start(
26
+ @ip,
27
+ @user,
28
+ keys: [@key_path],
29
+ non_interactive: true,
30
+ verify_host_key: :never
31
+ )
32
+
33
+ @session.forward.local(@local_port, "localhost", @remote_port)
34
+ @running = true
35
+
36
+ @thread = Thread.new do
37
+ Thread.current.report_on_exception = false
38
+ @session.loop { @running }
39
+ rescue IOError, Net::SSH::Disconnect, Errno::EBADF
40
+ # Session closed during shutdown, exit gracefully
41
+ end
42
+
43
+ # Wait for tunnel to establish
44
+ sleep 0.3
45
+ verify_tunnel!
46
+
47
+ Nvoi.logger.success "SSH tunnel established"
48
+ end
49
+
50
+ def stop
51
+ return unless @running
52
+
53
+ Nvoi.logger.info "Stopping SSH tunnel"
54
+ @running = false
55
+
56
+ # Give the event loop a moment to see @running = false
57
+ sleep 0.1
58
+
59
+ begin
60
+ @session&.forward&.cancel_local(@local_port)
61
+ rescue StandardError
62
+ # Ignore errors during cleanup
63
+ end
64
+
65
+ begin
66
+ @session&.close
67
+ rescue StandardError
68
+ # Ignore errors during cleanup
69
+ end
70
+
71
+ # Wait for thread to exit gracefully
72
+ @thread&.join(1)
73
+
74
+ @session = nil
75
+ @thread = nil
76
+
77
+ Nvoi.logger.success "SSH tunnel closed"
78
+ end
79
+
80
+ def alive?
81
+ @running && @thread&.alive? && @session && !@session.closed?
82
+ end
83
+
84
+ private
85
+
86
+ def verify_tunnel!
87
+ unless alive?
88
+ raise Errors::SshError, "SSH tunnel failed to start"
89
+ end
90
+
91
+ # Verify the port is actually listening
92
+ require "socket"
93
+ socket = TCPSocket.new("localhost", @local_port)
94
+ socket.close
95
+ rescue Errno::ECONNREFUSED
96
+ raise Errors::SshError, "SSH tunnel started but port #{@local_port} not accessible"
97
+ end
98
+ end
99
+ end
100
+ 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
@@ -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}"]
@@ -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.7"
4
+ VERSION = "0.2.0"
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