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
@@ -1,201 +0,0 @@
1
- # 08 - CLI: Deploy Steps (Details)
2
-
3
- ## Priority: FIFTH (part of deploy)
4
-
5
- Each step is a focused, single-responsibility class.
6
-
7
- ---
8
-
9
- ## Step 1: provision_network.rb (~50 lines)
10
-
11
- **Source:** Extract from `deployer/infrastructure.rb`
12
-
13
- **Does:**
14
-
15
- - Find or create network
16
- - Find or create firewall
17
-
18
- ```ruby
19
- # cli/deploy/steps/provision_network.rb
20
- module Nvoi
21
- module CLI
22
- module Deploy
23
- module Steps
24
- class ProvisionNetwork
25
- def initialize(config, provider, log)
26
- @config = config
27
- @provider = provider
28
- @log = log
29
- end
30
-
31
- def run
32
- @log.info "Provisioning network: %s", @config.network_name
33
- network = @provider.find_or_create_network(@config.network_name)
34
-
35
- @log.info "Provisioning firewall: %s", @config.firewall_name
36
- firewall = @provider.find_or_create_firewall(@config.firewall_name)
37
-
38
- { network: network, firewall: firewall }
39
- end
40
- end
41
- end
42
- end
43
- end
44
- end
45
- ```
46
-
47
- ---
48
-
49
- ## Step 2: provision_server.rb (~80 lines)
50
-
51
- **Source:** `steps/server_provisioner.rb` + `deployer/infrastructure.rb`
52
-
53
- **Does:**
54
-
55
- - Create servers for each group
56
- - Wait for SSH ready
57
- - Return main server IP
58
-
59
- ---
60
-
61
- ## Step 3: provision_volume.rb (~150 lines)
62
-
63
- **Source:** `steps/volume_provisioner.rb`
64
-
65
- **Does:**
66
-
67
- - Collect all volumes needed (database, services, app)
68
- - Create volumes via provider
69
- - Attach to servers
70
- - Mount via SSH (format if needed, add to fstab)
71
-
72
- ---
73
-
74
- ## Step 4: setup_k3s.rb (~400 lines)
75
-
76
- **Source:** Merge `steps/k3s_provisioner.rb` + `steps/k3s_cluster_setup.rb`
77
-
78
- **Does:**
79
-
80
- - Wait for cloud-init
81
- - Install K3s server on master
82
- - Setup kubeconfig
83
- - Setup in-cluster registry
84
- - Setup NGINX ingress
85
- - Install K3s agent on workers
86
- - Label nodes
87
- - Get cluster token
88
-
89
- **Note:** This is the largest step. Consider keeping as single file with private methods, or split into:
90
-
91
- - `setup_k3s/master.rb`
92
- - `setup_k3s/worker.rb`
93
- - `setup_k3s/registry.rb`
94
-
95
- ---
96
-
97
- ## Step 5: configure_tunnel.rb (~70 lines)
98
-
99
- **Source:** `steps/tunnel_configurator.rb`
100
-
101
- **Does:**
102
-
103
- - For each app service with domain:
104
- - Create/find Cloudflare tunnel
105
- - Configure tunnel ingress
106
- - Create DNS CNAME record
107
- - Return array of TunnelInfo
108
-
109
- ---
110
-
111
- ## Step 6: build_image.rb (~100 lines)
112
-
113
- **Source:** `deployer/image_builder.rb` + parts of `remote/docker_manager.rb`
114
-
115
- **Does:**
116
-
117
- - Build Docker image locally
118
- - Save to tar
119
- - Rsync to remote server
120
- - Import into containerd
121
- - Tag image
122
- - Push to in-cluster registry
123
-
124
- ---
125
-
126
- ## Step 7: deploy_database.rb (~80 lines)
127
-
128
- **Source:** `steps/database_provisioner.rb` + `deployer/service_deployer.rb#deploy_database`
129
-
130
- **Does:**
131
-
132
- - Skip if SQLite (handled by app volumes)
133
- - Deploy database secret (POSTGRES_USER, etc.)
134
- - Deploy StatefulSet with hostPath volume
135
- - Wait for database pod to be Running
136
-
137
- ---
138
-
139
- ## Step 8: deploy_service.rb (~300 lines)
140
-
141
- **Source:** Merge `deployer/orchestrator.rb` + `deployer/service_deployer.rb`
142
-
143
- **Does:**
144
-
145
- - Deploy app secret (env vars)
146
- - Deploy additional services (redis, etc.)
147
- - Deploy app services (web, worker)
148
- - Deploy cloudflared sidecars
149
- - Verify traffic routing
150
- - Run pre-run commands (migrations)
151
-
152
- **Note:** Uses `External::Kubectl` for all manifest operations.
153
-
154
- ---
155
-
156
- ## Step 9: cleanup_images.rb (~40 lines)
157
-
158
- **Source:** `deployer/cleaner.rb`
159
-
160
- **Does:**
161
-
162
- - List all images with prefix
163
- - Keep newest N images
164
- - Delete the rest
165
-
166
- ---
167
-
168
- ## Step Dependencies
169
-
170
- ```
171
- provision_network ─┐
172
- ├─► provision_server ─► provision_volume ─► setup_k3s
173
-
174
- configure_tunnel ──┼─► build_image ─► deploy_service ─► cleanup_images
175
-
176
- └── (parallel where possible)
177
- ```
178
-
179
- ---
180
-
181
- ## Common Patterns Across Steps
182
-
183
- ### Constructor Signature
184
-
185
- All steps follow:
186
-
187
- ```ruby
188
- def initialize(config, provider_or_deps, log)
189
- ```
190
-
191
- ### Return Values
192
-
193
- - Most return `nil` (side effects only)
194
- - Some return data for next step:
195
- - `provision_server` → main_server_ip
196
- - `configure_tunnel` → [TunnelInfo]
197
- - `provision_network` → { network:, firewall: }
198
-
199
- ### Error Handling
200
-
201
- Steps raise specific errors. Command catches and logs.
@@ -1,169 +0,0 @@
1
- # 09 - CLI: Delete Command
2
-
3
- ## Priority: FIFTH (parallel with deploy)
4
-
5
- ---
6
-
7
- ## Current State
8
-
9
- | File | Lines | Purpose |
10
- | ------------------- | ----- | -------------------------------------- |
11
- | `service/delete.rb` | 235 | DeleteService - teardown all resources |
12
-
13
- ---
14
-
15
- ## Target Structure
16
-
17
- ```
18
- lib/nvoi/cli/delete/
19
- ├── command.rb
20
- └── steps/
21
- ├── teardown_tunnel.rb
22
- ├── teardown_dns.rb
23
- ├── detach_volumes.rb
24
- ├── teardown_server.rb
25
- ├── teardown_volume.rb
26
- ├── teardown_firewall.rb
27
- └── teardown_network.rb
28
- ```
29
-
30
- ---
31
-
32
- ## What Delete Does (Current)
33
-
34
- ```ruby
35
- def run
36
- detach_volumes # Must happen before server deletion
37
- delete_all_servers # Delete servers from all groups
38
- delete_volumes # Now safe to delete
39
- delete_firewall # With retry (might still be attached)
40
- delete_network
41
- delete_cloudflare_resources # Tunnels + DNS records
42
- end
43
- ```
44
-
45
- ---
46
-
47
- ## Target Flow
48
-
49
- ```ruby
50
- # cli/delete/command.rb
51
- class Command
52
- def run
53
- config = Utils::ConfigLoader.new.load(config_path)
54
- provider = External::Cloud.for(config)
55
- cloudflare = External::DNS::Cloudflare.new(config.cloudflare)
56
- log = Utils::Logger.new
57
-
58
- # Order matters!
59
- Steps::DetachVolumes.new(config, provider, log).run
60
- Steps::TeardownServer.new(config, provider, log).run
61
- Steps::TeardownVolume.new(config, provider, log).run
62
- Steps::TeardownFirewall.new(config, provider, log).run
63
- Steps::TeardownNetwork.new(config, provider, log).run
64
- Steps::TeardownTunnel.new(config, cloudflare, log).run
65
- Steps::TeardownDNS.new(config, cloudflare, log).run
66
-
67
- log.success "Cleanup complete"
68
- end
69
- end
70
- ```
71
-
72
- ---
73
-
74
- ## DRY Opportunities
75
-
76
- ### 1. Shared Volume Name Collection
77
-
78
- Both `detach_volumes` and `delete_volumes` call `collect_volume_names`.
79
- Extract to utils or pass as step output:
80
-
81
- ```ruby
82
- volumes = Steps::DetachVolumes.new(...).run # returns volume list
83
- Steps::TeardownVolume.new(...).run(volumes)
84
- ```
85
-
86
- ### 2. Shared Hostname Builder
87
-
88
- `build_hostname` is duplicated. Already planned for `utils/namer.rb`.
89
-
90
- ### 3. Merge Tunnel + DNS Teardown
91
-
92
- Both iterate over app services with domains. Could be single step:
93
-
94
- ```ruby
95
- Steps::TeardownCloudflare.new(config, cloudflare, log).run
96
- # Deletes both tunnels and DNS records
97
- ```
98
-
99
- ### 4. Error Tolerance
100
-
101
- Delete operations should be idempotent. Current code does:
102
-
103
- ```ruby
104
- rescue StandardError => e
105
- @log.warning "Failed to delete: %s", e.message
106
- end
107
- ```
108
-
109
- Keep this pattern - deletion should continue even if some resources are already gone.
110
-
111
- ---
112
-
113
- ## Step Details
114
-
115
- ### detach_volumes.rb (~40 lines)
116
-
117
- - Collect all volume names
118
- - For each: find volume, detach if attached
119
-
120
- ### teardown_server.rb (~50 lines)
121
-
122
- - For each server group:
123
- - For each server in group:
124
- - Find and delete server
125
-
126
- ### teardown_volume.rb (~40 lines)
127
-
128
- - For each volume name:
129
- - Find and delete volume
130
-
131
- ### teardown_firewall.rb (~30 lines)
132
-
133
- - Find firewall by name
134
- - Delete with retry (may still be detaching)
135
-
136
- ### teardown_network.rb (~20 lines)
137
-
138
- - Find network by name
139
- - Delete
140
-
141
- ### teardown_tunnel.rb (~40 lines)
142
-
143
- - For each app service with domain:
144
- - Find and delete tunnel
145
-
146
- ### teardown_dns.rb (~40 lines)
147
-
148
- - For each app service with domain:
149
- - Find zone
150
- - Find and delete CNAME record
151
-
152
- ---
153
-
154
- ## Migration Steps
155
-
156
- 1. Create `lib/nvoi/cli/delete/command.rb`
157
- 2. Create `lib/nvoi/cli/delete/steps/` directory
158
- 3. Extract each operation from `service/delete.rb` into separate step
159
- 4. Move `collect_volume_names` to command (shared state)
160
- 5. Delete `service/delete.rb`
161
-
162
- ---
163
-
164
- ## Estimated Effort
165
-
166
- - **Lines to reorganize:** 235
167
- - **Files created:** 8 (command + 7 steps)
168
- - **Files deleted:** 1
169
- - **DRY savings:** ~30 lines (shared hostname, merged tunnel+dns option)
@@ -1,157 +0,0 @@
1
- # 10 - CLI: Exec Command
2
-
3
- ## Priority: FIFTH (parallel with deploy/delete)
4
-
5
- ---
6
-
7
- ## Current State
8
-
9
- | File | Lines | Purpose |
10
- |------|-------|---------|
11
- | `service/exec.rb` | 145 | ExecService - remote command execution |
12
-
13
- ---
14
-
15
- ## Target Structure
16
-
17
- ```
18
- lib/nvoi/cli/exec/
19
- └── command.rb # Single file, no steps needed
20
- ```
21
-
22
- ---
23
-
24
- ## What Exec Does
25
-
26
- Three modes:
27
- 1. **Single server:** `nvoi exec "ls -la"` → run on main server
28
- 2. **All servers:** `nvoi exec --all "ls -la"` → run on all servers in parallel
29
- 3. **Interactive:** `nvoi exec -i` → open SSH shell
30
-
31
- ---
32
-
33
- ## Target Implementation
34
-
35
- ```ruby
36
- # cli/exec/command.rb
37
- module Nvoi
38
- module CLI
39
- module Exec
40
- class Command
41
- def initialize(options)
42
- @options = options
43
- @log = Utils::Logger.new
44
- end
45
-
46
- def run(args)
47
- config = Utils::ConfigLoader.new.load(config_path)
48
- provider = External::Cloud.for(config)
49
-
50
- if @options[:interactive]
51
- open_shell(config, provider)
52
- elsif @options[:all]
53
- run_all(config, provider, args.join(" "))
54
- else
55
- run_single(config, provider, args.join(" "), @options[:server])
56
- end
57
- end
58
-
59
- private
60
-
61
- def run_single(config, provider, command, server_name)
62
- server = find_server(config, provider, server_name)
63
- ssh = External::SSH.new(server.public_ipv4, config.ssh_key_path)
64
-
65
- @log.info "Executing on %s: %s", server.name, command
66
- ssh.execute(command, stream: true)
67
- @log.success "Command completed"
68
- end
69
-
70
- def run_all(config, provider, command)
71
- servers = all_servers(config, provider)
72
- @log.info "Executing on %d servers", servers.size
73
-
74
- threads = servers.map do |server|
75
- Thread.new do
76
- ssh = External::SSH.new(server.public_ipv4, config.ssh_key_path)
77
- output = ssh.execute(command)
78
- [server.name, output]
79
- end
80
- end
81
-
82
- threads.each do |t|
83
- name, output = t.value
84
- output.lines.each { |line| puts "[#{name}] #{line}" }
85
- end
86
- end
87
-
88
- def open_shell(config, provider)
89
- server = find_server(config, provider, @options[:server])
90
- ssh = External::SSH.new(server.public_ipv4, config.ssh_key_path)
91
- ssh.open_shell
92
- end
93
-
94
- def find_server(config, provider, name)
95
- actual_name = resolve_server_name(config, name)
96
- provider.find_server(actual_name) or raise ServiceError, "server not found: #{actual_name}"
97
- end
98
-
99
- def resolve_server_name(config, name)
100
- return config.server_name if name == "main" || name.nil?
101
- # Parse "worker-1" → server_name("worker", 1)
102
- # ...
103
- end
104
-
105
- def all_servers(config, provider)
106
- config.deploy.application.servers.flat_map do |group, cfg|
107
- (1..cfg.count).map { |i| provider.find_server(config.namer.server_name(group, i)) }
108
- end.compact
109
- end
110
- end
111
- end
112
- end
113
- end
114
- ```
115
-
116
- ---
117
-
118
- ## DRY Opportunities
119
-
120
- ### 1. Server Name Resolution
121
- `resolve_server_name` logic is useful. Could go to `utils/namer.rb`:
122
- ```ruby
123
- # utils/namer.rb
124
- def resolve_server_reference(ref)
125
- # "main" → server_name for master
126
- # "worker-1" → server_name("worker", 1)
127
- end
128
- ```
129
-
130
- ### 2. Server Collection
131
- `all_servers` pattern could be a config method:
132
- ```ruby
133
- # objects/config.rb
134
- def each_server
135
- deploy.application.servers.each do |group, cfg|
136
- (1..cfg.count).each { |i| yield(group, i) }
137
- end
138
- end
139
- ```
140
-
141
- ---
142
-
143
- ## Migration Steps
144
-
145
- 1. Create `lib/nvoi/cli/exec/command.rb`
146
- 2. Move logic from `service/exec.rb`
147
- 3. Extract `resolve_server_name` to `utils/namer.rb`
148
- 4. Delete `service/exec.rb`
149
-
150
- ---
151
-
152
- ## Estimated Effort
153
-
154
- - **Lines to reorganize:** 145
155
- - **Files created:** 1
156
- - **Files deleted:** 1
157
- - **Net change:** ~0 (simple move + namespace change)
@@ -1,190 +0,0 @@
1
- # 11 - CLI: Credentials Commands
2
-
3
- ## Priority: FIFTH (parallel with others)
4
-
5
- ---
6
-
7
- ## Current State
8
-
9
- | File | Lines | Purpose |
10
- | ------------------------- | ----- | ------------------------------- |
11
- | `cli.rb` (CredentialsCLI) | ~55 | Thor subcommand for credentials |
12
- | `credentials/editor.rb` | ~80 | Edit/show encrypted credentials |
13
- | `credentials/manager.rb` | ~150 | Load/save encrypted files |
14
- | `credentials/crypto.rb` | ~100 | AES encryption |
15
-
16
- **Total: ~385 lines**
17
-
18
- ---
19
-
20
- ## Target Structure
21
-
22
- ```
23
- lib/nvoi/cli/credentials/
24
- ├── edit/
25
- │ └── command.rb # nvoi credentials edit
26
- └── show/
27
- └── command.rb # nvoi credentials show
28
- ```
29
-
30
- Note: `crypto.rb` and `manager.rb` move to `utils/` (see 02-utils.md)
31
-
32
- ---
33
-
34
- ## What Each Command Does
35
-
36
- ### credentials edit
37
-
38
- 1. Load or create credentials file
39
- 2. Decrypt to temp file
40
- 3. Open in $EDITOR
41
- 4. Validate YAML on save
42
- 5. Re-encrypt
43
- 6. Update .gitignore if first run
44
-
45
- ### credentials show
46
-
47
- 1. Load credentials file
48
- 2. Decrypt
49
- 3. Print to stdout
50
-
51
- ---
52
-
53
- ## Target Implementation
54
-
55
- ```ruby
56
- # cli/credentials/edit/command.rb
57
- module Nvoi
58
- module CLI
59
- module Credentials
60
- module Edit
61
- class Command
62
- def initialize(options)
63
- @options = options
64
- @log = Utils::Logger.new
65
- end
66
-
67
- def run
68
- working_dir = resolve_working_dir
69
- store = Utils::CredentialStore.new(working_dir, @options[:credentials], @options[:master_key])
70
-
71
- if store.exists?
72
- edit_existing(store)
73
- else
74
- create_new(store)
75
- end
76
- end
77
-
78
- private
79
-
80
- def edit_existing(store)
81
- content = store.read
82
- new_content = open_in_editor(content)
83
- validate_yaml!(new_content)
84
- store.write(new_content)
85
- @log.success "Credentials updated"
86
- end
87
-
88
- def create_new(store)
89
- @log.info "Creating new credentials file"
90
- template = default_template
91
- new_content = open_in_editor(template)
92
- validate_yaml!(new_content)
93
- store.write(new_content)
94
- store.update_gitignore
95
- @log.success "Credentials created"
96
- @log.warning "Keep your master key safe!"
97
- end
98
-
99
- def open_in_editor(content)
100
- # Write to temp file, exec $EDITOR, read back
101
- end
102
-
103
- def validate_yaml!(content)
104
- YAML.safe_load(content)
105
- rescue Psych::SyntaxError => e
106
- raise ConfigError, "Invalid YAML: #{e.message}"
107
- end
108
- end
109
- end
110
- end
111
- end
112
- end
113
- ```
114
-
115
- ```ruby
116
- # cli/credentials/show/command.rb
117
- module Nvoi
118
- module CLI
119
- module Credentials
120
- module Show
121
- class Command
122
- def initialize(options)
123
- @options = options
124
- end
125
-
126
- def run
127
- working_dir = resolve_working_dir
128
- store = Utils::CredentialStore.new(working_dir, @options[:credentials], @options[:master_key])
129
- puts store.read
130
- end
131
- end
132
- end
133
- end
134
- end
135
- end
136
- ```
137
-
138
- ---
139
-
140
- ## DRY Opportunities
141
-
142
- ### 1. Merge Crypto + Manager → CredentialStore
143
-
144
- Already planned in 02-utils.md. Single class:
145
-
146
- ```ruby
147
- # utils/crypto.rb
148
- class CredentialStore
149
- def initialize(working_dir, enc_path = nil, key_path = nil)
150
- def exists? → bool
151
- def read → String
152
- def write(content)
153
- def update_gitignore
154
- end
155
- ```
156
-
157
- ### 2. Remove Editor Class
158
-
159
- Current `credentials/editor.rb` is thin wrapper. Logic goes directly into command.
160
-
161
- ### 3. YAML Validation
162
-
163
- Move to utils for reuse:
164
-
165
- ```ruby
166
- # utils/config_loader.rb
167
- def self.validate_yaml!(content)
168
- YAML.safe_load(content, permitted_classes: [Symbol])
169
- end
170
- ```
171
-
172
- ---
173
-
174
- ## Migration Steps
175
-
176
- 1. Move `credentials/crypto.rb` + `credentials/manager.rb` → `utils/crypto.rb` (02-utils.md)
177
- 2. Create `lib/nvoi/cli/credentials/edit/command.rb`
178
- 3. Create `lib/nvoi/cli/credentials/show/command.rb`
179
- 4. Move editor logic into `edit/command.rb`
180
- 5. Delete `credentials/editor.rb`
181
- 6. Update `cli.rb` to route to new commands
182
-
183
- ---
184
-
185
- ## Estimated Effort
186
-
187
- - **Lines to reorganize:** ~385
188
- - **Files created:** 2
189
- - **Files deleted:** 3 (`editor.rb`, `manager.rb`, `crypto.rb` → merged to utils)
190
- - **Net reduction:** ~50 lines (removed wrapper, merged classes)