nvoi 0.1.5 → 0.1.7

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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/todo/refactor/00-overview.md +171 -0
  3. data/.claude/todo/refactor/01-objects.md +96 -0
  4. data/.claude/todo/refactor/02-utils.md +143 -0
  5. data/.claude/todo/refactor/03-external-cloud.md +164 -0
  6. data/.claude/todo/refactor/04-external-dns.md +104 -0
  7. data/.claude/todo/refactor/05-external.md +133 -0
  8. data/.claude/todo/refactor/06-cli.md +123 -0
  9. data/.claude/todo/refactor/07-cli-deploy-command.md +177 -0
  10. data/.claude/todo/refactor/08-cli-deploy-steps.md +201 -0
  11. data/.claude/todo/refactor/09-cli-delete-command.md +169 -0
  12. data/.claude/todo/refactor/10-cli-exec-command.md +157 -0
  13. data/.claude/todo/refactor/11-cli-credentials-command.md +190 -0
  14. data/.claude/todo/refactor/12-cli-db-command.md +128 -0
  15. data/.claude/todo/refactor/_target.md +79 -0
  16. data/.claude/todo/refactor-execution/00-entrypoint.md +49 -0
  17. data/.claude/todo/refactor-execution/01-objects.md +42 -0
  18. data/.claude/todo/refactor-execution/02-utils.md +41 -0
  19. data/.claude/todo/refactor-execution/03-external-cloud.md +38 -0
  20. data/.claude/todo/refactor-execution/04-external-dns.md +35 -0
  21. data/.claude/todo/refactor-execution/05-external-other.md +46 -0
  22. data/.claude/todo/refactor-execution/06-cli-deploy.md +45 -0
  23. data/.claude/todo/refactor-execution/07-cli-delete.md +43 -0
  24. data/.claude/todo/refactor-execution/08-cli-exec.md +30 -0
  25. data/.claude/todo/refactor-execution/09-cli-credentials.md +34 -0
  26. data/.claude/todo/refactor-execution/10-cli-db.md +31 -0
  27. data/.claude/todo/refactor-execution/11-cli-router.md +44 -0
  28. data/.claude/todo/refactor-execution/12-cleanup.md +120 -0
  29. data/.claude/todo/refactor-execution/_monitoring-strategy.md +126 -0
  30. data/.claude/todo/scaleway.impl.md +644 -0
  31. data/.claude/todo/scaleway.reference.md +520 -0
  32. data/.claude/todos.md +550 -0
  33. data/Gemfile +6 -0
  34. data/Gemfile.lock +46 -5
  35. data/Rakefile +1 -1
  36. data/doc/config-schema.yaml +44 -11
  37. data/examples/golang/deploy.enc +0 -0
  38. data/examples/golang/main.go +18 -0
  39. data/exe/nvoi +3 -1
  40. data/ingest +0 -0
  41. data/lib/nvoi/cli/config/command.rb +219 -0
  42. data/lib/nvoi/cli/credentials/edit/command.rb +384 -0
  43. data/lib/nvoi/cli/credentials/show/command.rb +35 -0
  44. data/lib/nvoi/cli/db/command.rb +308 -0
  45. data/lib/nvoi/cli/delete/command.rb +75 -0
  46. data/lib/nvoi/cli/delete/steps/detach_volumes.rb +98 -0
  47. data/lib/nvoi/cli/delete/steps/teardown_dns.rb +50 -0
  48. data/lib/nvoi/cli/delete/steps/teardown_firewall.rb +46 -0
  49. data/lib/nvoi/cli/delete/steps/teardown_network.rb +30 -0
  50. data/lib/nvoi/cli/delete/steps/teardown_server.rb +50 -0
  51. data/lib/nvoi/cli/delete/steps/teardown_tunnel.rb +44 -0
  52. data/lib/nvoi/cli/delete/steps/teardown_volume.rb +61 -0
  53. data/lib/nvoi/cli/deploy/command.rb +184 -0
  54. data/lib/nvoi/cli/deploy/steps/build_image.rb +27 -0
  55. data/lib/nvoi/cli/deploy/steps/cleanup_images.rb +42 -0
  56. data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +102 -0
  57. data/lib/nvoi/cli/deploy/steps/deploy_service.rb +399 -0
  58. data/lib/nvoi/cli/deploy/steps/provision_network.rb +44 -0
  59. data/lib/nvoi/cli/deploy/steps/provision_server.rb +143 -0
  60. data/lib/nvoi/cli/deploy/steps/provision_volume.rb +171 -0
  61. data/lib/nvoi/cli/deploy/steps/setup_k3s.rb +490 -0
  62. data/lib/nvoi/cli/exec/command.rb +173 -0
  63. data/lib/nvoi/cli/logs/command.rb +66 -0
  64. data/lib/nvoi/cli/onboard/command.rb +761 -0
  65. data/lib/nvoi/cli/unlock/command.rb +72 -0
  66. data/lib/nvoi/cli.rb +339 -141
  67. data/lib/nvoi/config_api/actions/app.rb +53 -0
  68. data/lib/nvoi/config_api/actions/compute_provider.rb +55 -0
  69. data/lib/nvoi/config_api/actions/database.rb +70 -0
  70. data/lib/nvoi/config_api/actions/domain_provider.rb +40 -0
  71. data/lib/nvoi/config_api/actions/env.rb +32 -0
  72. data/lib/nvoi/config_api/actions/init.rb +67 -0
  73. data/lib/nvoi/config_api/actions/secret.rb +32 -0
  74. data/lib/nvoi/config_api/actions/server.rb +66 -0
  75. data/lib/nvoi/config_api/actions/service.rb +52 -0
  76. data/lib/nvoi/config_api/actions/volume.rb +40 -0
  77. data/lib/nvoi/config_api/base.rb +38 -0
  78. data/lib/nvoi/config_api/result.rb +26 -0
  79. data/lib/nvoi/config_api.rb +93 -0
  80. data/lib/nvoi/errors.rb +68 -50
  81. data/lib/nvoi/external/cloud/aws.rb +450 -0
  82. data/lib/nvoi/external/cloud/base.rb +99 -0
  83. data/lib/nvoi/external/cloud/factory.rb +48 -0
  84. data/lib/nvoi/external/cloud/hetzner.rb +402 -0
  85. data/lib/nvoi/external/cloud/scaleway.rb +559 -0
  86. data/lib/nvoi/external/cloud.rb +15 -0
  87. data/lib/nvoi/external/containerd.rb +86 -0
  88. data/lib/nvoi/external/database/mysql.rb +84 -0
  89. data/lib/nvoi/external/database/postgres.rb +82 -0
  90. data/lib/nvoi/external/database/provider.rb +65 -0
  91. data/lib/nvoi/external/database/sqlite.rb +72 -0
  92. data/lib/nvoi/external/database.rb +22 -0
  93. data/lib/nvoi/external/dns/cloudflare.rb +310 -0
  94. data/lib/nvoi/external/kubectl.rb +65 -0
  95. data/lib/nvoi/external/ssh.rb +106 -0
  96. data/lib/nvoi/objects/config_override.rb +60 -0
  97. data/lib/nvoi/objects/configuration.rb +483 -0
  98. data/lib/nvoi/objects/database.rb +56 -0
  99. data/lib/nvoi/objects/dns.rb +14 -0
  100. data/lib/nvoi/objects/firewall.rb +11 -0
  101. data/lib/nvoi/objects/network.rb +11 -0
  102. data/lib/nvoi/objects/server.rb +14 -0
  103. data/lib/nvoi/objects/service_spec.rb +26 -0
  104. data/lib/nvoi/objects/tunnel.rb +14 -0
  105. data/lib/nvoi/objects/volume.rb +17 -0
  106. data/lib/nvoi/utils/config_loader.rb +172 -0
  107. data/lib/nvoi/utils/constants.rb +61 -0
  108. data/lib/nvoi/{credentials/manager.rb → utils/credential_store.rb} +16 -16
  109. data/lib/nvoi/{credentials → utils}/crypto.rb +8 -5
  110. data/lib/nvoi/{config → utils}/env_resolver.rb +10 -2
  111. data/lib/nvoi/utils/logger.rb +84 -0
  112. data/lib/nvoi/{config/naming.rb → utils/namer.rb} +37 -25
  113. data/lib/nvoi/{deployer → utils}/retry.rb +23 -3
  114. data/lib/nvoi/utils/templates.rb +62 -0
  115. data/lib/nvoi/version.rb +1 -1
  116. data/lib/nvoi.rb +27 -55
  117. data/templates/app-ingress.yaml.erb +3 -1
  118. data/templates/error-backend.yaml.erb +134 -0
  119. metadata +121 -44
  120. data/examples/golang/deploy.yml +0 -54
  121. data/lib/nvoi/cloudflare/client.rb +0 -287
  122. data/lib/nvoi/config/config.rb +0 -248
  123. data/lib/nvoi/config/loader.rb +0 -102
  124. data/lib/nvoi/config/ssh_keys.rb +0 -82
  125. data/lib/nvoi/config/types.rb +0 -274
  126. data/lib/nvoi/constants.rb +0 -59
  127. data/lib/nvoi/credentials/editor.rb +0 -272
  128. data/lib/nvoi/deployer/cleaner.rb +0 -36
  129. data/lib/nvoi/deployer/image_builder.rb +0 -23
  130. data/lib/nvoi/deployer/infrastructure.rb +0 -126
  131. data/lib/nvoi/deployer/orchestrator.rb +0 -146
  132. data/lib/nvoi/deployer/service_deployer.rb +0 -311
  133. data/lib/nvoi/deployer/tunnel_manager.rb +0 -57
  134. data/lib/nvoi/deployer/types.rb +0 -8
  135. data/lib/nvoi/k8s/renderer.rb +0 -44
  136. data/lib/nvoi/k8s/templates.rb +0 -29
  137. data/lib/nvoi/logger.rb +0 -72
  138. data/lib/nvoi/providers/aws.rb +0 -403
  139. data/lib/nvoi/providers/base.rb +0 -111
  140. data/lib/nvoi/providers/hetzner.rb +0 -288
  141. data/lib/nvoi/providers/hetzner_client.rb +0 -170
  142. data/lib/nvoi/remote/docker_manager.rb +0 -203
  143. data/lib/nvoi/remote/ssh_executor.rb +0 -72
  144. data/lib/nvoi/remote/volume_manager.rb +0 -103
  145. data/lib/nvoi/service/delete.rb +0 -234
  146. data/lib/nvoi/service/deploy.rb +0 -80
  147. data/lib/nvoi/service/exec.rb +0 -144
  148. data/lib/nvoi/service/provider.rb +0 -36
  149. data/lib/nvoi/steps/application_deployer.rb +0 -26
  150. data/lib/nvoi/steps/database_provisioner.rb +0 -60
  151. data/lib/nvoi/steps/k3s_cluster_setup.rb +0 -105
  152. data/lib/nvoi/steps/k3s_provisioner.rb +0 -351
  153. data/lib/nvoi/steps/server_provisioner.rb +0 -43
  154. data/lib/nvoi/steps/services_provisioner.rb +0 -29
  155. data/lib/nvoi/steps/tunnel_configurator.rb +0 -66
  156. data/lib/nvoi/steps/volume_provisioner.rb +0 -154
@@ -0,0 +1,384 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+ require "set"
5
+
6
+ module Nvoi
7
+ class Cli
8
+ module Credentials
9
+ module Edit
10
+ # Command handles editing encrypted credentials
11
+ class Command
12
+ DEFAULT_ENCRYPTED_FILE = "deploy.enc"
13
+ DEFAULT_KEY_FILE = "deploy.key"
14
+ DEFAULT_EDITOR = "vim"
15
+ TEMP_FILE_PATTERN = "nvoi-credentials-"
16
+
17
+ def initialize(options)
18
+ @options = options
19
+ @log = Nvoi.logger
20
+ @editor = ENV["EDITOR"] || DEFAULT_EDITOR
21
+ end
22
+
23
+ def run
24
+ @log.info "Credentials Editor"
25
+
26
+ working_dir = resolve_working_dir
27
+ enc_path = resolve_enc_path(working_dir)
28
+ is_first_time = !File.exist?(enc_path)
29
+
30
+ manager = if is_first_time
31
+ @log.info "Creating new encrypted credentials file"
32
+ Utils::CredentialStore.for_init(working_dir)
33
+ else
34
+ Utils::CredentialStore.new(working_dir, @options[:credentials], @options[:master_key])
35
+ end
36
+
37
+ # Get initial content
38
+ content = if is_first_time
39
+ default_template
40
+ else
41
+ manager.read
42
+ end
43
+
44
+ # Create temp file
45
+ tmp_file = Tempfile.new([TEMP_FILE_PATTERN, ".yaml"])
46
+ tmp_path = tmp_file.path
47
+
48
+ begin
49
+ tmp_file.write(content)
50
+ tmp_file.close
51
+
52
+ # Edit loop: keep opening editor until valid or user quits
53
+ loop do
54
+ # Get file mtime before edit
55
+ before_mtime = File.mtime(tmp_path)
56
+
57
+ # Open editor
58
+ unless system(@editor, tmp_path)
59
+ raise Errors::CredentialError, "editor failed"
60
+ end
61
+
62
+ # Check if file was modified
63
+ after_mtime = File.mtime(tmp_path)
64
+ if after_mtime == before_mtime
65
+ puts "No changes made, aborting."
66
+ return
67
+ end
68
+
69
+ # Read edited content
70
+ edited_content = File.read(tmp_path)
71
+
72
+ # Validate
73
+ validation_error = validate(edited_content)
74
+ if validation_error
75
+ puts "\n\e[31mValidation failed:\e[0m #{validation_error}"
76
+ puts "\nPress Enter to re-edit, or Ctrl+C to abort..."
77
+ $stdin.gets
78
+ next
79
+ end
80
+
81
+ # Valid: save
82
+ if is_first_time
83
+ manager.initialize_credentials(edited_content)
84
+ else
85
+ manager.write(edited_content)
86
+ end
87
+
88
+ puts "\e[32mCredentials saved:\e[0m #{manager.encrypted_path}"
89
+ break
90
+ end
91
+ ensure
92
+ tmp_file.close rescue nil
93
+ tmp_file.unlink rescue nil
94
+ end
95
+
96
+ # Update .gitignore on first run
97
+ if manager.key_path
98
+ begin
99
+ update_gitignore(working_dir)
100
+ @log.info "Added %s to .gitignore", DEFAULT_KEY_FILE
101
+ rescue StandardError => e
102
+ @log.warning "Failed to update .gitignore: %s", e.message
103
+ end
104
+
105
+ @log.success "Master key saved to: %s", manager.key_path
106
+ @log.warning "Keep this key safe! You cannot decrypt credentials without it."
107
+ end
108
+ end
109
+
110
+ def set(path, value)
111
+ @log.info "Setting credential value"
112
+
113
+ working_dir = resolve_working_dir
114
+ manager = Utils::CredentialStore.new(working_dir, @options[:credentials], @options[:master_key])
115
+
116
+ # Read current content
117
+ content = manager.read
118
+ data = YAML.safe_load(content, permitted_classes: [Symbol])
119
+
120
+ # Navigate path and set value
121
+ keys = path.split(".")
122
+ current = data
123
+
124
+ # Handle 'application.' prefix - it's implied
125
+ keys.shift if keys.first == "application"
126
+
127
+ # Navigate to parent
128
+ keys[0..-2].each do |key|
129
+ current["application"] ||= {}
130
+ current = current["application"]
131
+ current[key] ||= {}
132
+ current = current[key]
133
+ end
134
+
135
+ # Set the value
136
+ if keys.length == 1
137
+ data["application"] ||= {}
138
+ data["application"][keys.last] = value
139
+ else
140
+ current[keys.last] = value
141
+ end
142
+
143
+ # Write back
144
+ new_content = YAML.dump(data)
145
+ manager.write(new_content)
146
+
147
+ @log.success "Updated: %s = %s", path, value
148
+ end
149
+
150
+ private
151
+
152
+ def resolve_working_dir
153
+ wd = @options[:dir]
154
+ if wd.nil? || wd.empty? || wd == "."
155
+ Dir.pwd
156
+ else
157
+ File.expand_path(wd)
158
+ end
159
+ end
160
+
161
+ def resolve_enc_path(working_dir)
162
+ enc_path = @options[:credentials]
163
+ return File.join(working_dir, DEFAULT_ENCRYPTED_FILE) if enc_path.nil? || enc_path.empty?
164
+
165
+ enc_path
166
+ end
167
+
168
+ def validate(content)
169
+ # First: basic YAML parse
170
+ begin
171
+ data = YAML.safe_load(content, permitted_classes: [Symbol])
172
+ rescue Psych::SyntaxError => e
173
+ return "invalid YAML syntax: #{e.message}"
174
+ end
175
+
176
+ return "config must be a hash" unless data.is_a?(Hash)
177
+
178
+ # Second: validate required fields
179
+ validate_required_fields(data)
180
+ end
181
+
182
+ def validate_required_fields(cfg)
183
+ app = cfg["application"]
184
+ return "application section is required" unless app.is_a?(Hash)
185
+
186
+ # Application name
187
+ return "application.name is required" if app["name"].nil? || app["name"].to_s.empty?
188
+
189
+ # Environment
190
+ return "application.environment is required" if app["environment"].nil? || app["environment"].to_s.empty?
191
+
192
+ # Domain provider
193
+ domain_provider = app["domain_provider"]
194
+ return "application.domain_provider.cloudflare is required" unless domain_provider&.dig("cloudflare")
195
+
196
+ cf = domain_provider["cloudflare"]
197
+ return "application.domain_provider.cloudflare.api_token is required" if cf["api_token"].nil? || cf["api_token"].to_s.empty?
198
+ return "application.domain_provider.cloudflare.account_id is required" if cf["account_id"].nil? || cf["account_id"].to_s.empty?
199
+
200
+ # Compute provider
201
+ compute_provider = app["compute_provider"]
202
+ has_compute = compute_provider&.dig("hetzner") || compute_provider&.dig("aws") || compute_provider&.dig("scaleway")
203
+ return "compute_provider (hetzner, aws, or scaleway) is required" unless has_compute
204
+
205
+ if (h = compute_provider&.dig("hetzner"))
206
+ return "application.compute_provider.hetzner.api_token is required" if h["api_token"].nil? || h["api_token"].to_s.empty?
207
+ return "application.compute_provider.hetzner.server_type is required" if h["server_type"].nil? || h["server_type"].to_s.empty?
208
+ return "application.compute_provider.hetzner.server_location is required" if h["server_location"].nil? || h["server_location"].to_s.empty?
209
+ end
210
+
211
+ if (a = compute_provider&.dig("aws"))
212
+ return "application.compute_provider.aws.access_key_id is required" if a["access_key_id"].nil? || a["access_key_id"].to_s.empty?
213
+ return "application.compute_provider.aws.secret_access_key is required" if a["secret_access_key"].nil? || a["secret_access_key"].to_s.empty?
214
+ return "application.compute_provider.aws.region is required" if a["region"].nil? || a["region"].to_s.empty?
215
+ return "application.compute_provider.aws.instance_type is required" if a["instance_type"].nil? || a["instance_type"].to_s.empty?
216
+ end
217
+
218
+ if (s = compute_provider&.dig("scaleway"))
219
+ return "application.compute_provider.scaleway.secret_key is required" if s["secret_key"].nil? || s["secret_key"].to_s.empty?
220
+ return "application.compute_provider.scaleway.project_id is required" if s["project_id"].nil? || s["project_id"].to_s.empty?
221
+ return "application.compute_provider.scaleway.server_type is required" if s["server_type"].nil? || s["server_type"].to_s.empty?
222
+ end
223
+
224
+ # Servers (if any services defined)
225
+ servers = app["servers"] || {}
226
+ app_services = app["app"] || {}
227
+ database = app["database"]
228
+ services = app["services"] || {}
229
+
230
+ has_services = !app_services.empty? || database || !services.empty?
231
+ return "servers must be defined when deploying services" if has_services && servers.empty?
232
+
233
+ defined_servers = servers.keys.to_set
234
+
235
+ # Validate app services
236
+ app_services.each do |service_name, svc|
237
+ next unless svc
238
+
239
+ return "app.#{service_name}.servers is required" if svc["servers"].nil? || svc["servers"].empty?
240
+
241
+ svc["servers"].each do |ref|
242
+ return "app.#{service_name} references undefined server: #{ref}" unless defined_servers.include?(ref)
243
+ end
244
+ end
245
+
246
+ # Validate database
247
+ if database
248
+ return "database.servers is required" if database["servers"].nil? || database["servers"].empty?
249
+
250
+ database["servers"].each do |ref|
251
+ return "database references undefined server: #{ref}" unless defined_servers.include?(ref)
252
+ end
253
+
254
+ db_error = validate_database_secrets(database)
255
+ return db_error if db_error
256
+ end
257
+
258
+ # Validate SSH keys
259
+ ssh_keys = app["ssh_keys"]
260
+ return "application.ssh_keys is required" unless ssh_keys.is_a?(Hash)
261
+ return "application.ssh_keys.private_key is required" if ssh_keys["private_key"].nil? || ssh_keys["private_key"].to_s.strip.empty?
262
+ return "application.ssh_keys.public_key is required" if ssh_keys["public_key"].nil? || ssh_keys["public_key"].to_s.strip.empty?
263
+
264
+ nil
265
+ end
266
+
267
+ def validate_database_secrets(db)
268
+ adapter = db["adapter"]&.downcase
269
+ url = db["url"]
270
+
271
+ return "database.adapter is required" if adapter.nil? || adapter.empty?
272
+
273
+ # URL takes precedence - if provided, no secrets needed
274
+ has_url = url && !url.to_s.empty?
275
+
276
+ case adapter
277
+ when "postgres", "postgresql"
278
+ return nil if has_url
279
+
280
+ %w[POSTGRES_USER POSTGRES_PASSWORD POSTGRES_DB].each do |key|
281
+ return "database.secrets.#{key} is required for postgres (or provide database.url)" unless db.dig("secrets", key)
282
+ end
283
+ when "mysql"
284
+ return nil if has_url
285
+
286
+ %w[MYSQL_USER MYSQL_PASSWORD MYSQL_DATABASE].each do |key|
287
+ return "database.secrets.#{key} is required for mysql (or provide database.url)" unless db.dig("secrets", key)
288
+ end
289
+ when "sqlite", "sqlite3"
290
+ # SQLite doesn't require secrets - path can be inferred from url, mount, or defaults
291
+ else
292
+ return "unsupported database adapter: #{adapter}"
293
+ end
294
+
295
+ nil
296
+ end
297
+
298
+ def default_template
299
+ # Generate SSH keypair for first-time setup
300
+ private_key, public_key = Utils::ConfigLoader.generate_keypair
301
+
302
+ <<~YAML
303
+ # NVOI Deployment Configuration
304
+ # This file is encrypted - never commit deploy.key!
305
+
306
+ application:
307
+ name: myapp
308
+ environment: production
309
+
310
+ domain_provider:
311
+ cloudflare:
312
+ api_token: YOUR_CLOUDFLARE_API_TOKEN
313
+ account_id: YOUR_CLOUDFLARE_ACCOUNT_ID
314
+
315
+ compute_provider:
316
+ hetzner:
317
+ api_token: YOUR_HETZNER_API_TOKEN
318
+ server_type: cx22
319
+ server_location: fsn1
320
+
321
+ servers:
322
+ master:
323
+ type: cx22
324
+ location: fsn1
325
+
326
+ keep_count: 2
327
+
328
+ app:
329
+ web:
330
+ servers: [master]
331
+ domain: example.com
332
+ subdomain: app
333
+ port: 3000
334
+ healthcheck:
335
+ type: http
336
+ path: /health
337
+ port: 3000
338
+
339
+ # database:
340
+ # servers: [master]
341
+ # adapter: postgres
342
+ # url: postgres://myapp:YOUR_DB_PASSWORD@localhost:5432/myapp_production
343
+ # image: postgres:16-alpine
344
+ #
345
+ # Or for SQLite (no container needed):
346
+ # database:
347
+ # servers: [master]
348
+ # adapter: sqlite3
349
+ # mount:
350
+ # db: /app/data
351
+
352
+ env:
353
+ # Add environment variables here
354
+ # RAILS_ENV: production
355
+
356
+ secrets:
357
+ # Add secrets here (will be injected as env vars)
358
+ # SECRET_KEY_BASE: YOUR_SECRET_KEY_BASE
359
+
360
+ # SSH keys (auto-generated, do not modify)
361
+ ssh_keys:
362
+ private_key: |
363
+ #{private_key.lines.map { |l| " #{l}" }.join}
364
+ public_key: #{public_key}
365
+ YAML
366
+ end
367
+
368
+ def update_gitignore(working_dir)
369
+ gitignore_path = File.join(working_dir, ".gitignore")
370
+ existing = File.exist?(gitignore_path) ? File.read(gitignore_path) : ""
371
+
372
+ return if existing.include?(DEFAULT_KEY_FILE)
373
+
374
+ File.open(gitignore_path, "a") do |f|
375
+ f.puts "" unless existing.end_with?("\n") || existing.empty?
376
+ f.puts "# Nvoi master key - DO NOT COMMIT"
377
+ f.puts DEFAULT_KEY_FILE
378
+ end
379
+ end
380
+ end
381
+ end
382
+ end
383
+ end
384
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ class Cli
5
+ module Credentials
6
+ module Show
7
+ # Command handles displaying decrypted credentials
8
+ class Command
9
+ def initialize(options)
10
+ @options = options
11
+ end
12
+
13
+ def run
14
+ working_dir = resolve_working_dir
15
+ manager = Utils::CredentialStore.new(working_dir, @options[:credentials], @options[:master_key])
16
+
17
+ content = manager.read
18
+ puts content
19
+ end
20
+
21
+ private
22
+
23
+ def resolve_working_dir
24
+ wd = @options[:dir]
25
+ if wd.nil? || wd.empty? || wd == "."
26
+ Dir.pwd
27
+ else
28
+ File.expand_path(wd)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end