dnsmadeeasy 0.4.0 → 1.0.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +5 -10
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +5 -4
  5. data/.rubocop_todo.yml +251 -143
  6. data/CLAUDE.md +67 -0
  7. data/Gemfile +9 -1
  8. data/README.md +858 -0
  9. data/Rakefile +4 -4
  10. data/dnsmadeeasy.gemspec +42 -26
  11. data/docs/badges/coverage_badge.svg +21 -0
  12. data/docs/dry-cli-based-tools.webloc +8 -0
  13. data/docs/plan-zone-management.md +756 -0
  14. data/docs/plans/01-plan-ruby4-baseline.md +38 -0
  15. data/docs/plans/02-plan-dry-cli-launcher.md +44 -0
  16. data/docs/plans/03-plan-account-command.md +43 -0
  17. data/docs/plans/04-plan-zone-record-model.md +48 -0
  18. data/docs/plans/05-plan-zone-parser.md +41 -0
  19. data/docs/plans/06-plan-zone-formatter.md +43 -0
  20. data/docs/plans/07-plan-zone-export.md +58 -0
  21. data/docs/plans/08-plan-zone-diff.md +42 -0
  22. data/docs/plans/09-plan-zone-apply.md +69 -0
  23. data/docs/plans/10-plan-docs-cleanup.md +55 -0
  24. data/docs/spec-zone-management.md +242 -0
  25. data/exe/dme +13 -2
  26. data/exe/dmez +6 -0
  27. data/lib/dme.rb +7 -2
  28. data/lib/dnsmadeeasy/api/client.rb +32 -26
  29. data/lib/dnsmadeeasy/cli/box_output.rb +9 -0
  30. data/lib/dnsmadeeasy/cli/commands/account.rb +222 -0
  31. data/lib/dnsmadeeasy/cli/commands/base.rb +119 -0
  32. data/lib/dnsmadeeasy/cli/commands/legacy_operation.rb +30 -0
  33. data/lib/dnsmadeeasy/cli/commands/version.rb +22 -0
  34. data/lib/dnsmadeeasy/cli/commands/zone.rb +326 -0
  35. data/lib/dnsmadeeasy/cli/commands.rb +19 -0
  36. data/lib/dnsmadeeasy/cli/input.rb +14 -0
  37. data/lib/dnsmadeeasy/cli/launcher.rb +53 -0
  38. data/lib/dnsmadeeasy/cli/message_helpers.rb +81 -0
  39. data/lib/dnsmadeeasy/cli/reported_error.rb +10 -0
  40. data/lib/dnsmadeeasy/credentials/api_keys.rb +11 -9
  41. data/lib/dnsmadeeasy/credentials.rb +0 -1
  42. data/lib/dnsmadeeasy/runner.rb +24 -24
  43. data/lib/dnsmadeeasy/types.rb +19 -0
  44. data/lib/dnsmadeeasy/version.rb +2 -1
  45. data/lib/dnsmadeeasy/zone/aname_flattener.rb +63 -0
  46. data/lib/dnsmadeeasy/zone/apply_executor.rb +189 -0
  47. data/lib/dnsmadeeasy/zone/apply_result.rb +22 -0
  48. data/lib/dnsmadeeasy/zone/diff.rb +152 -0
  49. data/lib/dnsmadeeasy/zone/file.rb +26 -0
  50. data/lib/dnsmadeeasy/zone/parser.rb +172 -0
  51. data/lib/dnsmadeeasy/zone/plan.rb +28 -0
  52. data/lib/dnsmadeeasy/zone/plan_action.rb +29 -0
  53. data/lib/dnsmadeeasy/zone/plan_renderer.rb +91 -0
  54. data/lib/dnsmadeeasy/zone/provider_record.rb +18 -0
  55. data/lib/dnsmadeeasy/zone/record.rb +44 -0
  56. data/lib/dnsmadeeasy/zone/record_set.rb +23 -0
  57. data/lib/dnsmadeeasy/zone/remote_adapter.rb +94 -0
  58. data/lib/dnsmadeeasy/zone/remote_records.rb +26 -0
  59. data/lib/dnsmadeeasy/zone/serializer.rb +115 -0
  60. data/lib/dnsmadeeasy.rb +61 -27
  61. metadata +184 -25
  62. data/.dme-help.png +0 -0
  63. data/README.adoc +0 -690
@@ -0,0 +1,756 @@
1
+ # Plan: Ruby 4, dry-cli, and Zone File Management
2
+
3
+ ## Context
4
+
5
+ This gem is currently a compact Ruby API client and CLI for DNS Made Easy. The CLI is implemented by `DnsMadeEasy::Runner`, which forwards arbitrary operation names directly to `DnsMadeEasy.client`.
6
+
7
+ The requested direction is a major-version refactor:
8
+
9
+ - drop Ruby 2 support
10
+ - support Ruby 4
11
+ - bump the gem to `1.0.0`
12
+ - replace the current CLI runner with `dry-cli`
13
+ - add stronger validation using focused dry-rb gems
14
+ - preserve existing API operations under an `account` command namespace
15
+ - add declarative zone-file workflows under a new `zone` command namespace
16
+
17
+ The zone-management specification lives in `docs/spec-zone-management.md`.
18
+
19
+ ## Source Material Reviewed
20
+
21
+ - Local current implementation:
22
+ - `lib/dnsmadeeasy/api/client.rb`
23
+ - `lib/dnsmadeeasy/runner.rb`
24
+ - `lib/dnsmadeeasy/credentials*.rb`
25
+ - `exe/dme`
26
+ - current specs
27
+ - Local reference project:
28
+ - `../githuh/lib/githuh/cli/launcher.rb`
29
+ - `../githuh/lib/githuh/cli/commands/base.rb`
30
+ - `../githuh/spec/support/aruba_helper.rb`
31
+ - `../githuh/spec/spec_helper.rb`
32
+ - External references:
33
+ - dry-rb/Hanakai overview and dry CLI/type/validation docs index
34
+ - Konstantin Gredeskoul's CLI + Aruba article
35
+ - `aeden/dns-zonefile` README
36
+
37
+ ## Design Principles
38
+
39
+ 1. Keep `DnsMadeEasy::Api::Client` as the provider boundary.
40
+ 2. Treat `dry-cli` as command dispatch only, not as business logic.
41
+ 3. Introduce typed value objects before introducing diff/apply logic.
42
+ 4. Make every command testable in process through Aruba.
43
+ 5. Keep zone parsing isolated behind an adapter so `DNS::Zonefile::*` classes do not leak through the implementation.
44
+ 6. Make `plan` the safety gate for `apply`.
45
+ 7. Keep deletion opt-in and out of the first implementation unless explicitly requested.
46
+ 8. Use `tty-spinner` for long-running groups of independent provider operations.
47
+
48
+ ## Parallel Provider Operations
49
+
50
+ The gem includes `tty-spinner` for visual progress during multi-step provider work. Zone workflows should split independent API work into bounded chunks, execute those chunks with worker threads, and attach one `TTY::Spinner` to each active worker or chunk.
51
+
52
+ Use `TTY::Spinner::Multi` for managing multiple concurrent thread progress indicators:
53
+ https://github.com/piotrmurach/tty-spinner#5-ttyspinnermulti-api
54
+
55
+ Rules:
56
+
57
+ - Use bounded concurrency; do not start one unbounded thread per DNS record.
58
+ - Only parallelize independent API calls.
59
+ - Preserve deterministic final output by collecting results and sorting/rendering after workers finish.
60
+ - Keep `plan` rendering deterministic and mostly non-animated; spinners belong to long-running `export` and `apply` execution paths.
61
+ - Provide a no-op or fake spinner in specs so test output is stable.
62
+ - Aggregate worker errors and report which chunk/action failed.
63
+ - Avoid parallel delete behavior until destructive operations are explicitly enabled and tested.
64
+
65
+ ## Zone File Compatibility and Normalization
66
+
67
+ The `aeden/dns-zonefile` gem is sufficient for version 1 parsing. We are comfortable using it to parse downloaded or standard RFC-style zone files, and we accept its current limitations because this feature does not need to preserve every possible BIND extension or provider-specific behavior.
68
+
69
+ Export output is not intended to round-trip arbitrary zone-file formatting. Instead, `dme zone export` and `dme zone fmt` produce a normalized, readable, deterministic zone file.
70
+
71
+ Normalized zone characteristics:
72
+
73
+ - one zone per file
74
+ - explicit owner on every record
75
+ - `$ORIGIN`
76
+ - `$TTL`
77
+ - `@` for zone apex
78
+ - deterministic ordering
79
+ - normalized, aligned whitespace
80
+ - provider-managed SOA omitted
81
+ - apex NS records omitted by default
82
+ - delegated NS records preserved
83
+
84
+ Example:
85
+
86
+ ```dns
87
+ $ORIGIN example.com.
88
+ $TTL 300
89
+
90
+ @ IN A 203.0.113.10
91
+ www IN CNAME @
92
+
93
+ @ IN MX 10 mail.example.com.
94
+
95
+ @ IN TXT "v=spf1 include:_spf.google.com ~all"
96
+ ```
97
+
98
+ `HTTPRED` cannot be represented in a standard RFC zone file.
99
+
100
+ Version 1 behavior:
101
+
102
+ - omit `HTTPRED` records from exports
103
+ - emit a warning listing omitted records
104
+
105
+ Future versions may support provider-specific comment directives for records such as `HTTPRED`, but version 1 should keep the exported zone file standard and predictable.
106
+
107
+ ## Phase 0: Baseline and Version Policy
108
+
109
+ ### Objective
110
+
111
+ Establish a clean Ruby 4 baseline before changing CLI architecture.
112
+
113
+ ### Work
114
+
115
+ 1. Decide the exact supported Ruby range.
116
+ - Recommended: `spec.required_ruby_version = '>= 3.2', '< 4.1'` initially.
117
+ - If the goal is Ruby 4 only, use `~> 4.0`, matching the `githuh` reference.
118
+ - Do not keep Ruby 2 compatibility constraints.
119
+ 2. Bump `DnsMadeEasy::VERSION` from `0.4.0` to `1.0.0`.
120
+ 3. Update the GitHub Actions workflow.
121
+ - Replace Ruby 2.7 with Ruby 4.0.
122
+ - Use current `ruby/setup-ruby`.
123
+ - Enable Bundler cache.
124
+ 4. Add or update dependencies.
125
+ - Runtime:
126
+ - `dry-cli`
127
+ - `dry-struct`
128
+ - `dry-types`
129
+ - `dry-validation`
130
+ - `dns-zonefile`
131
+ - `tsort`, because `sym` warns that `tsort` will no longer be bundled by default in Ruby 4.1
132
+ - `tty-spinner`
133
+ - Development:
134
+ - `aruba`
135
+ 5. Run the current suite.
136
+
137
+ ### Acceptance Criteria
138
+
139
+ - `bundle exec rspec` passes on Ruby 4.
140
+ - CI targets Ruby 4.
141
+ - Version reports `1.0.0`.
142
+ - Ruby 2 support is explicitly removed from gem metadata and CI.
143
+
144
+ ## Phase 1: Introduce dry-cli Shell Without Changing Behavior
145
+
146
+ ### Objective
147
+
148
+ Add a dry-cli launcher and command registry while preserving the current user-visible behavior as much as possible.
149
+
150
+ ### Proposed Files
151
+
152
+ ```text
153
+ lib/dnsmadeeasy/cli/launcher.rb
154
+ lib/dnsmadeeasy/cli/commands/base.rb
155
+ lib/dnsmadeeasy/cli/commands/version.rb
156
+ lib/dnsmadeeasy/cli/commands/account.rb
157
+ spec/support/aruba_helper.rb
158
+ spec/lib/dnsmadeeasy/cli/launcher_spec.rb
159
+ spec/lib/dnsmadeeasy/cli/commands/version_spec.rb
160
+ spec/lib/dnsmadeeasy/cli/commands/account_spec.rb
161
+ ```
162
+
163
+ ### Work
164
+
165
+ 1. Add `DnsMadeEasy::CLI::Commands` as a `Dry::CLI::Registry`.
166
+ 2. Add `DnsMadeEasy::CLI::Launcher`.
167
+ - Accept `argv`, `stdin`, `stdout`, `stderr`, and optional `kernel`.
168
+ - Send all command output through injected streams.
169
+ - Do not call raw `STDOUT`, `STDERR`, or `Kernel.exit` from command classes.
170
+ 3. Update `exe/dme` to instantiate the launcher:
171
+
172
+ ```ruby
173
+ DnsMadeEasy::CLI::Launcher.new(ARGV.dup).execute!
174
+ ```
175
+
176
+ 4. Add `Base < Dry::CLI::Command`.
177
+ - Centralize authentication setup.
178
+ - Centralize output format helpers.
179
+ - Centralize client creation.
180
+ - Support common options:
181
+ - `--credentials-file`
182
+ - `--account`
183
+ - `--sandbox`
184
+ - `--format=json|json_pretty|yaml|pp`
185
+ - `--verbose`
186
+ 5. Add Aruba in-process test setup based on `../githuh`.
187
+ 6. Keep `DnsMadeEasy::Runner` temporarily.
188
+ - Mark it internal/deprecated.
189
+ - Use it as an implementation reference while migrating commands.
190
+
191
+ ### Acceptance Criteria
192
+
193
+ - `dme --help` is handled by dry-cli.
194
+ - `dme version` prints `1.0.0`.
195
+ - Aruba can run `dme` in-process.
196
+ - Existing library specs still pass.
197
+
198
+ ## Phase 2: Move Existing API Operations Under `account`
199
+
200
+ ### Objective
201
+
202
+ Create the compatibility namespace for existing API operations:
203
+
204
+ ```text
205
+ dme account <operation> [args...] [--format=...]
206
+ ```
207
+
208
+ Examples:
209
+
210
+ ```text
211
+ dme account domains
212
+ dme account domain example.com
213
+ dme account records_for example.com
214
+ dme account create_a_record example.com www 203.0.113.10
215
+ ```
216
+
217
+ The user proposal used `dme account create_record A ...`; the existing API shape is `create_record DOMAIN NAME TYPE VALUE`. Keep the existing method shape first. Add friendlier aliases later if needed.
218
+
219
+ ### Work
220
+
221
+ 1. Implement `DnsMadeEasy::CLI::Commands::Account`.
222
+ 2. Use `DnsMadeEasy::Api::Client.public_operations` to validate allowed operation names.
223
+ 3. Treat the operation name as an argument, not a generated class per API method.
224
+ - This minimizes refactoring.
225
+ - It avoids generating dozens of tiny command classes with no separate behavior.
226
+ 4. Forward remaining CLI arguments to the client method.
227
+ 5. Preserve existing output formats.
228
+ 6. Preserve useful failure behavior:
229
+ - unknown operation: print valid operations
230
+ - wrong arity: print method signature when discoverable
231
+ - API failures: non-zero exit and readable error
232
+ 7. Add a hidden or explicit compatibility shim if needed:
233
+ - Option A: keep old `dme domains` behavior through a root fallback command.
234
+ - Option B: make `dme domains` fail with a clear migration hint.
235
+ - Recommended for `1.0.0`: fail with a migration hint because this is a major version.
236
+
237
+ ### Acceptance Criteria
238
+
239
+ - `dme account operations` lists supported API operations.
240
+ - `dme account records_for example.com --format=json` works with a mocked client in specs.
241
+ - Existing runner specs are replaced by Aruba specs.
242
+ - `DnsMadeEasy::Runner` can be deleted after parity is covered.
243
+
244
+ ## Phase 3: Add Typed Domain Model for DNS Records
245
+
246
+ ### Objective
247
+
248
+ Create a provider-neutral internal record model used by export, parse, diff, plan, and apply.
249
+
250
+ ### Proposed Files
251
+
252
+ ```text
253
+ lib/dnsmadeeasy/types.rb
254
+ lib/dnsmadeeasy/zone/record.rb
255
+ lib/dnsmadeeasy/zone/record_key.rb
256
+ lib/dnsmadeeasy/zone/record_set.rb
257
+ lib/dnsmadeeasy/zone/domain_name.rb
258
+ lib/dnsmadeeasy/zone/ttl.rb
259
+ ```
260
+
261
+ ### Recommended dry-rb Usage
262
+
263
+ 1. `dry-types`
264
+ - DNS name type
265
+ - TTL type
266
+ - record type enum
267
+ - non-empty string type
268
+ - priority/weight/port integer types
269
+ 2. `dry-struct`
270
+ - immutable record value objects
271
+ - execution plan objects
272
+ 3. `dry-validation`
273
+ - command input validation
274
+ - zone-level validation that needs cross-field rules
275
+
276
+ ### Initial Record Types
277
+
278
+ Support the standard records already represented by the existing client:
279
+
280
+ - `A`
281
+ - `AAAA`
282
+ - `CNAME`
283
+ - `MX`
284
+ - `NS`
285
+ - `PTR`
286
+ - `SPF`
287
+ - `SRV`
288
+ - `TXT`
289
+
290
+ Explicitly omit `HTTPRED` from zone files because it is provider-specific and cannot be represented in a standard zone file.
291
+
292
+ ### Internal Model Sketch
293
+
294
+ ```ruby
295
+ module DnsMadeEasy
296
+ module Zone
297
+ class Record < Dry::Struct
298
+ attribute :owner, Types::String
299
+ attribute :type, Types::RecordType
300
+ attribute :value, Types::String
301
+ attribute :ttl, Types::Integer.constrained(gteq: 0)
302
+ attribute :priority, Types::Integer.optional
303
+ attribute :weight, Types::Integer.optional
304
+ attribute :port, Types::Integer.optional
305
+ end
306
+ end
307
+ end
308
+ ```
309
+
310
+ Do not include provider IDs in `Record`. Store remote metadata separately.
311
+
312
+ ### Acceptance Criteria
313
+
314
+ - Unit tests can construct valid records.
315
+ - Invalid record types fail fast.
316
+ - Record equality ignores provider metadata.
317
+ - Records are sortable deterministically.
318
+
319
+ ## Phase 4: Zone File Parser Adapter
320
+
321
+ ### Objective
322
+
323
+ Use `dns-zonefile` only at the parsing boundary.
324
+
325
+ The parser adapter should rely on `dns-zonefile` as sufficient for downloaded or standard zone files. Any unsupported syntax should become an explicit validation error rather than leaking parser internals into the rest of the application.
326
+
327
+ ### Proposed Files
328
+
329
+ ```text
330
+ lib/dnsmadeeasy/zone/parser.rb
331
+ lib/dnsmadeeasy/zone/parser_adapter.rb
332
+ lib/dnsmadeeasy/zone/parse_result.rb
333
+ spec/fixtures/zones/example.com.zone
334
+ spec/lib/dnsmadeeasy/zone/parser_spec.rb
335
+ ```
336
+
337
+ ### Work
338
+
339
+ 1. Add `dns-zonefile`.
340
+ 2. Implement a parser adapter around:
341
+
342
+ ```ruby
343
+ DNS::Zonefile.load(zone_string, origin)
344
+ ```
345
+
346
+ 3. Convert all returned records immediately into `DnsMadeEasy::Zone::Record`.
347
+ 4. Normalize:
348
+ - empty owner to `@`
349
+ - origin-qualified owner names
350
+ - TTL inheritance
351
+ - TXT quoting
352
+ - MX priority
353
+ - SRV priority, weight, and port
354
+ 5. Reject or warn on unsupported records.
355
+ 6. Treat parser exceptions as command validation failures.
356
+
357
+ ### Acceptance Criteria
358
+
359
+ - `zone validate FILE` can parse a fixture zone file.
360
+ - `DNS::Zonefile::*` objects never leave the parser adapter.
361
+ - Unsupported records produce actionable validation output.
362
+
363
+ ## Phase 5: Canonical Zone Serializer and Formatter
364
+
365
+ ### Objective
366
+
367
+ Produce deterministic, Git-friendly zone-file output.
368
+
369
+ ### Proposed Files
370
+
371
+ ```text
372
+ lib/dnsmadeeasy/zone/serializer.rb
373
+ lib/dnsmadeeasy/zone/formatter.rb
374
+ spec/lib/dnsmadeeasy/zone/serializer_spec.rb
375
+ spec/lib/dnsmadeeasy/zone/formatter_spec.rb
376
+ ```
377
+
378
+ ### Rules
379
+
380
+ The serializer emits the normalized zone-file format described in `Zone File Compatibility and Normalization`.
381
+
382
+ 1. Emit one zone per file.
383
+ 2. Emit `$ORIGIN`.
384
+ 3. Emit `$TTL`.
385
+ 4. Emit explicit owner on every record.
386
+ 5. Use `@` for apex records.
387
+ 6. Omit provider-managed SOA.
388
+ 7. Omit apex NS records by default.
389
+ 8. Preserve delegated NS records.
390
+ 9. Sort deterministically:
391
+ - owner
392
+ - type priority group
393
+ - priority
394
+ - value
395
+ - ttl
396
+ 10. Normalize whitespace.
397
+
398
+ ### Commands
399
+
400
+ ```text
401
+ dme zone validate FILE
402
+ dme zone fmt FILE
403
+ ```
404
+
405
+ Use `fmt` as the canonical command name, matching the spec. Optionally add `format` as an alias.
406
+
407
+ ### Acceptance Criteria
408
+
409
+ - Parse then serialize then parse returns equivalent records.
410
+ - Formatting an already formatted zone is byte-identical.
411
+ - `dme zone fmt FILE --check` can be added later without changing internals.
412
+
413
+ ## Phase 6: Remote Record Adapter
414
+
415
+ ### Objective
416
+
417
+ Convert DNS Made Easy API responses into the same internal record model used by zone files.
418
+
419
+ ### Proposed Files
420
+
421
+ ```text
422
+ lib/dnsmadeeasy/zone/provider_record.rb
423
+ lib/dnsmadeeasy/zone/remote_reader.rb
424
+ spec/lib/dnsmadeeasy/zone/remote_reader_spec.rb
425
+ ```
426
+
427
+ ### Work
428
+
429
+ 1. Use the existing API client:
430
+
431
+ ```ruby
432
+ client.records_for(domain)
433
+ ```
434
+
435
+ 2. Convert `Hashie::Mash` responses into internal records plus provider metadata.
436
+ 3. Keep metadata such as record ID separate:
437
+
438
+ ```ruby
439
+ ProviderRecord = Struct.new(:record, :provider_id, keyword_init: true)
440
+ ```
441
+
442
+ or a `Dry::Struct` equivalent.
443
+
444
+ 4. Omit `HTTPRED` from exports and emit warnings.
445
+ 5. Filter provider-managed SOA and apex NS according to serializer rules.
446
+
447
+ ### Acceptance Criteria
448
+
449
+ - Remote records convert to internal records with IDs preserved separately.
450
+ - HTTPRED records are reported but omitted from zone serialization.
451
+ - Export fixtures do not include provider IDs.
452
+
453
+ ## Phase 7: `zone export`
454
+
455
+ ### Objective
456
+
457
+ Download remote provider records and emit a canonical zone file.
458
+
459
+ ### Command
460
+
461
+ ```text
462
+ dme zone export DOMAIN [--output=FILE] [--ttl=300] [--include-apex-ns]
463
+ ```
464
+
465
+ ### Work
466
+
467
+ 1. Fetch remote records.
468
+ 2. Convert via remote adapter.
469
+ 3. Serialize canonical zone file.
470
+ 4. Write to stdout by default.
471
+ 5. Write to file when `--output` is provided.
472
+ 6. Print warnings to stderr, not stdout, so stdout remains pipe-safe.
473
+ 7. Use threaded chunks and `TTY::Spinner` when export requires multiple independent provider calls.
474
+
475
+ ### Acceptance Criteria
476
+
477
+ - Export of the same mocked records is byte-identical across runs.
478
+ - HTTPRED omission warning appears on stderr.
479
+ - Stdout contains only zone-file content.
480
+
481
+ ## Phase 8: Diff Engine and Execution Plan
482
+
483
+ ### Objective
484
+
485
+ Compare desired zone-file state with remote state and produce a human-readable plan.
486
+
487
+ ### Proposed Files
488
+
489
+ ```text
490
+ lib/dnsmadeeasy/zone/diff.rb
491
+ lib/dnsmadeeasy/zone/plan.rb
492
+ lib/dnsmadeeasy/zone/plan_action.rb
493
+ lib/dnsmadeeasy/zone/plan_renderer.rb
494
+ spec/lib/dnsmadeeasy/zone/diff_spec.rb
495
+ spec/lib/dnsmadeeasy/zone/plan_renderer_spec.rb
496
+ ```
497
+
498
+ ### Semantics
499
+
500
+ Default behavior:
501
+
502
+ - create missing records
503
+ - update modified records
504
+ - never delete records
505
+
506
+ Future behavior:
507
+
508
+ - `--delete` allows removal of remote records absent from the zone file, excluding protected records.
509
+
510
+ ### Record Identity
511
+
512
+ Use a stable identity key:
513
+
514
+ ```text
515
+ owner + type + routing fields
516
+ ```
517
+
518
+ For simple records, `owner + type` may not be sufficient because multiple TXT, MX, NS, and A records can coexist. The diff engine should model record sets, not assume one record per owner/type.
519
+
520
+ Recommended approach:
521
+
522
+ - compare sets by full record content for creates/deletes
523
+ - compare update candidates only where DNS Made Easy requires update instead of delete/create
524
+ - prefer create/delete pair over ambiguous update when multiple records share owner/type
525
+
526
+ This is the part where sloppy code becomes a DNS outage generator, so do not invent clever matching without tests.
527
+
528
+ ### Command
529
+
530
+ ```text
531
+ dme zone plan FILE [--domain=DOMAIN] [--format=text|json]
532
+ ```
533
+
534
+ If `$ORIGIN` is present, `--domain` is optional. If no `$ORIGIN` exists, require `--domain`.
535
+
536
+ ### Acceptance Criteria
537
+
538
+ - Plan output is deterministic.
539
+ - Creates, updates, and skipped deletes are separately listed.
540
+ - Ambiguous updates are either resolved safely or reported as requiring manual intervention.
541
+ - `plan --format=json` is machine-readable for later automation.
542
+
543
+ ## Phase 9: `zone apply`
544
+
545
+ ### Objective
546
+
547
+ Execute a reviewed plan using the existing API client.
548
+
549
+ ### Command
550
+
551
+ ```text
552
+ dme zone apply FILE [--domain=DOMAIN] [--yes] [--delete]
553
+ ```
554
+
555
+ ### Work
556
+
557
+ 1. Reuse the same planner used by `zone plan`.
558
+ 2. Refuse to apply destructive actions unless `--delete` is present.
559
+ 3. Require confirmation unless `--yes` is present.
560
+ 4. Execute creates and updates through existing client methods.
561
+ 5. Execute deletes only when explicitly enabled.
562
+ 6. Print a final summary.
563
+ 7. Split independent API operations into bounded worker-thread chunks and display a `TTY::Spinner` for each active worker/chunk.
564
+
565
+ ### Safety Rules
566
+
567
+ - Never delete by default.
568
+ - Never delete provider-managed SOA.
569
+ - Never delete apex NS unless an explicit future option allows it.
570
+ - Fail before partial application if validation fails.
571
+ - Consider ordering:
572
+ - create replacement records before deleting old records where safe
573
+ - avoid CNAME conflicts by detecting incompatible desired state before apply
574
+ - Parallelize only independent actions after dependency/conflict checks.
575
+ - Keep execution summaries deterministic even when worker completion order varies.
576
+
577
+ ### Acceptance Criteria
578
+
579
+ - `apply` uses exactly the plan generated from current remote state.
580
+ - `apply` can run against a mocked client in specs.
581
+ - Confirmation is tested through Aruba.
582
+ - Partial failures report completed and failed actions.
583
+ - Threaded execution has specs for successful chunks and failed chunks.
584
+ - Spinner behavior is covered through injected fake/no-op spinner objects.
585
+
586
+ ## Phase 10: Documentation and Migration Notes
587
+
588
+ ### Work
589
+
590
+ 1. Update README from old flat CLI to namespaced CLI.
591
+ 2. Document Ruby support and 1.0.0 breaking changes.
592
+ 3. Add examples:
593
+
594
+ ```text
595
+ dme account domains
596
+ dme account records_for example.com
597
+ dme zone export example.com > example.com.zone
598
+ dme zone validate example.com.zone
599
+ dme zone fmt example.com.zone
600
+ dme zone plan example.com.zone
601
+ dme zone apply example.com.zone --yes
602
+ ```
603
+
604
+ 4. Add a migration section:
605
+
606
+ ```text
607
+ dme domains
608
+ # becomes
609
+ dme account domains
610
+ ```
611
+
612
+ 5. Document unsupported records:
613
+ - `HTTPRED` omitted from standard zone files
614
+ - future provider-specific directives may be added
615
+
616
+ ### Acceptance Criteria
617
+
618
+ - README examples are covered by Aruba smoke specs where practical.
619
+ - The old command style is either removed with a clear message or documented as unsupported in 1.0.0.
620
+
621
+ ## Proposed Implementation Order
622
+
623
+ 1. Runtime baseline:
624
+ - version `1.0.0`
625
+ - Ruby 4 support
626
+ - dependencies
627
+ - CI
628
+ 2. dry-cli launcher:
629
+ - command registry
630
+ - version command
631
+ - Aruba in-process setup
632
+ 3. `account` command:
633
+ - existing API operation forwarding
634
+ - output formats
635
+ - delete `Runner` once parity exists
636
+ 4. zone model:
637
+ - dry types
638
+ - dry structs
639
+ - validation contracts
640
+ 5. parser and serializer:
641
+ - `validate`
642
+ - `fmt`
643
+ 6. remote adapter and export:
644
+ - `export`
645
+ 7. diff and plan:
646
+ - `plan`
647
+ 8. apply:
648
+ - confirmation
649
+ - execution
650
+ - failure reporting
651
+ 9. documentation and examples.
652
+
653
+ ## Test Strategy
654
+
655
+ ### Unit Specs
656
+
657
+ - typed record validation
658
+ - parser adapter
659
+ - serializer
660
+ - remote adapter
661
+ - diff engine
662
+ - plan renderer
663
+
664
+ ### CLI Specs
665
+
666
+ Use Aruba in-process tests for:
667
+
668
+ - `dme --help`
669
+ - `dme version`
670
+ - `dme account operations`
671
+ - `dme account records_for example.com`
672
+ - `dme zone validate FILE`
673
+ - `dme zone fmt FILE`
674
+ - `dme zone export DOMAIN`
675
+ - `dme zone plan FILE`
676
+ - `dme zone apply FILE --yes`
677
+
678
+ ### API Specs
679
+
680
+ Keep WebMock around existing client tests. Do not hit real DNS Made Easy in automated tests.
681
+
682
+ ### Live Integration Specs
683
+
684
+ There is one real DNS Made Easy domain available for opt-in integration testing:
685
+
686
+ - Domain: `isdue.today`
687
+ - DNS Made Easy domain ID: `8218117`
688
+
689
+ This domain is real but not currently in use. Live specs may use it to verify actual provider behavior for export, plan, and apply.
690
+
691
+ Rules:
692
+
693
+ - Never run live integration specs in default CI.
694
+ - Gate them behind an explicit environment variable such as `DNSMADEEASY_LIVE_TESTS=1`.
695
+ - Require real credentials through the existing credential mechanisms.
696
+ - Scope mutations to predictable test records, for example `_dnsmadeeasy-test.isdue.today`.
697
+ - Clean up records created by the test.
698
+ - Never modify apex records, NS records, SOA records, MX records, or any record not created by the test run.
699
+ - Make live specs idempotent so a failed run can be retried safely.
700
+
701
+ ### Golden Files
702
+
703
+ Use fixture zone files and expected output files for:
704
+
705
+ - canonical formatting
706
+ - export output
707
+ - plan text output
708
+ - plan JSON output
709
+
710
+ ## Open Decisions
711
+
712
+ 1. Ruby support:
713
+ - Ruby 4 only, or Ruby 3.2+ plus Ruby 4?
714
+ - Recommendation: Ruby 4 only if this gem is primarily for current internal use; Ruby 3.2+ if public gem compatibility matters.
715
+ 2. Backward CLI compatibility:
716
+ - Should `dme domains` continue working as an alias for `dme account domains`?
717
+ - Recommendation: no, because this is a major version and the new command tree should be unambiguous.
718
+ 3. Zone-file SOA handling:
719
+ - Should `validate` permit SOA but `fmt/export` omit it?
720
+ - Recommendation: parse and validate SOA if present, but never apply SOA changes in version 1.
721
+ 4. Multiple records with same owner/type:
722
+ - Should updates be represented as delete/create unless there is a single unambiguous remote candidate?
723
+ - Recommendation: yes.
724
+ 5. Apex NS behavior:
725
+ - Should `export --include-apex-ns` exist in version 1?
726
+ - Recommendation: yes for visibility, but default remains omit.
727
+
728
+ ## Risks
729
+
730
+ 1. `dns-zonefile` may not support every zone-file construct users expect.
731
+ - Mitigation: keep adapter isolated and surface precise validation errors.
732
+ 2. DNS Made Easy record semantics may not map perfectly to RFC zone records.
733
+ - Mitigation: keep provider metadata separate and document omissions.
734
+ 3. Diffing DNS records can be deceptively dangerous.
735
+ - Mitigation: default to no deletes, deterministic plans, and conservative ambiguity handling.
736
+ 4. CLI refactor can accidentally break existing automation.
737
+ - Mitigation: major version bump, migration docs, and Aruba tests.
738
+ 5. `sym` may produce Ruby 4.1 dependency warnings.
739
+ - Mitigation: add `tsort` explicitly and consider replacing `sym` later.
740
+
741
+ ## Definition of Done
742
+
743
+ - Gem version is `1.0.0`.
744
+ - Ruby 2 support is removed.
745
+ - CI runs on Ruby 4.
746
+ - CLI is dry-cli based and testable with Aruba.
747
+ - Existing account operations are available under `dme account`.
748
+ - Zone commands exist:
749
+ - `export`
750
+ - `validate`
751
+ - `fmt`
752
+ - `plan`
753
+ - `apply`
754
+ - Zone files are canonical and deterministic.
755
+ - Apply is safe by default and never deletes without explicit opt-in.
756
+ - README documents the new command tree and migration path.