@startanaicompany/saac_dns 1.1.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.
@@ -0,0 +1,1338 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { Command } = require('commander');
5
+ const Table = require('cli-table3');
6
+ const readline = require('readline');
7
+ const dns = require('../index');
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('saac_dns')
13
+ .description('SAAC DNS — Programmatic Domain Registration & DNS Management CLI')
14
+ .version('1.0.0')
15
+ .option('--json', 'Output raw JSON')
16
+ .option('--quiet', 'Minimal output')
17
+ .option('--verbose', 'Verbose output');
18
+
19
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
20
+
21
+ function isJson() { return program.opts().json; }
22
+ function isQuiet() { return program.opts().quiet; }
23
+ function isVerbose() { return program.opts().verbose; }
24
+
25
+ /** Interactive yes/no prompt using readline (Node.js-safe replacement for browser confirm()) */
26
+ function rlConfirm(question) {
27
+ // In non-TTY mode (pipes, --json, CI) auto-confirm to avoid hanging
28
+ if (!process.stdin.isTTY || isJson() || isQuiet()) return Promise.resolve(true);
29
+ return new Promise((resolve) => {
30
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
31
+ rl.question(`${question} [y/N] `, (answer) => {
32
+ rl.close();
33
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
34
+ });
35
+ });
36
+ }
37
+
38
+ function output(data, tableRenderer) {
39
+ if (isJson()) {
40
+ console.log(JSON.stringify(data, null, 2));
41
+ } else if (isQuiet()) {
42
+ // --quiet: suppress all table/formatted output; only tick() messages are shown
43
+ return;
44
+ } else if (isVerbose()) {
45
+ // --verbose: show table AND raw JSON response
46
+ if (tableRenderer) tableRenderer(data);
47
+ console.error('\n[verbose] Raw response:');
48
+ console.error(JSON.stringify(data, null, 2));
49
+ } else if (tableRenderer) {
50
+ tableRenderer(data);
51
+ } else {
52
+ console.log(JSON.stringify(data, null, 2));
53
+ }
54
+ }
55
+
56
+ function handleError(err) {
57
+ if (isJson()) {
58
+ console.error(JSON.stringify({ error: err.message, code: err.code }));
59
+ } else {
60
+ console.error(`✗ ${err.message}`);
61
+ }
62
+ process.exit(1);
63
+ }
64
+
65
+ function tick(msg) { if (!isQuiet() && !isJson()) console.log(`✓ ${msg}`); }
66
+
67
+ // ─── search ──────────────────────────────────────────────────────────────────
68
+
69
+ program
70
+ .command('search <domains...>')
71
+ .description('Check domain availability and pricing')
72
+ .option('--buy', 'Register the domain if available')
73
+ .action(async (domains, opts) => {
74
+ try {
75
+ const results = await dns.search(domains);
76
+
77
+ output(results, (data) => {
78
+ const table = new Table({
79
+ head: ['Domain', 'Status', 'Price', 'TLD'],
80
+ style: { head: ['cyan'] }
81
+ });
82
+ data.forEach(r => {
83
+ table.push([
84
+ r.fqdn,
85
+ r.available ? '✓ Available' : '✗ Taken',
86
+ r.price ? `$${r.price}/yr` : '—',
87
+ r.tld
88
+ ]);
89
+ });
90
+ console.log(table.toString());
91
+ });
92
+
93
+ if (opts.buy) {
94
+ const available = results.filter(r => r.available);
95
+ for (const r of available) {
96
+ const confirmed = await rlConfirm(`Register ${r.fqdn} for $${r.price}/yr?`);
97
+ if (confirmed) {
98
+ const res = await dns.buy(r.fqdn);
99
+ tick(`Domain ${r.fqdn} registered!`);
100
+ if (isJson()) console.log(JSON.stringify(res, null, 2));
101
+ }
102
+ }
103
+ }
104
+ } catch (err) { handleError(err); }
105
+ });
106
+
107
+ // ─── buy ─────────────────────────────────────────────────────────────────────
108
+
109
+ program
110
+ .command('buy <domain>')
111
+ .description('Register a domain')
112
+ .option('--years <n>', 'Registration years', '1')
113
+ .option('--privacy', 'Enable WHOIS privacy', false)
114
+ .option('--no-auto-renew', 'Disable auto-renewal')
115
+ .action(async (domain, opts) => {
116
+ try {
117
+ const result = await dns.buy(domain, {
118
+ years: parseInt(opts.years),
119
+ privacy: opts.privacy,
120
+ autoRenew: opts.autoRenew !== false
121
+ });
122
+ if (result.status === 'pending_confirmation') {
123
+ tick(`Purchase initiated for ${domain} — confirmation required`);
124
+ output(result, (r) => {
125
+ console.log(`\n Confirmation email sent. Expires in ${Math.floor(r.expiresIn / 60)} min.`);
126
+ console.log(` Token: ${r.token}`);
127
+ console.log(` Run: saac_dns buy-confirm ${r.token}`);
128
+ });
129
+ } else {
130
+ tick(`Domain ${domain} registered successfully!`);
131
+ output(result, (d) => {
132
+ console.log(` Domain: ${d.domain?.fqdn || domain}`);
133
+ console.log(` Status: ${d.domain?.status || 'active'}`);
134
+ console.log(` Expires: ${d.domain?.expires_at ? new Date(d.domain.expires_at).toDateString() : 'in 1 year'}`);
135
+ console.log(` Price: $${d.price}/yr`);
136
+ console.log(` Auto-renew: ${d.domain?.auto_renew ? 'on' : 'off'}`);
137
+ console.log(` Nameservers: ${(d.domain?.nameservers || ['ns1.saac.dns','ns2.saac.dns']).join(', ')}`);
138
+ });
139
+ }
140
+ } catch (err) { handleError(err); }
141
+ });
142
+
143
+ // ─── buy-confirm ─────────────────────────────────────────────────────────────
144
+
145
+ program
146
+ .command('buy-confirm <token>')
147
+ .description('Confirm a pending domain purchase using the token from buy')
148
+ .action(async (token) => {
149
+ try {
150
+ const result = await dns.confirmPurchase(token);
151
+ tick(`Domain ${result.domain?.fqdn || result.domain} registered successfully`);
152
+ output(result, (r) => {
153
+ const d = r.domain;
154
+ if (d) {
155
+ console.log(` Domain: ${d.fqdn}`);
156
+ console.log(` Status: ${d.status}`);
157
+ console.log(` Expires: ${d.expires_at}`);
158
+ console.log(` Price: $${r.price}/yr`);
159
+ }
160
+ });
161
+ } catch (err) { handleError(err); }
162
+ });
163
+
164
+ // ─── buy-drop ─────────────────────────────────────────────────────────────────
165
+ program
166
+ .command('buy-drop <domain>')
167
+ .description('Register an expiring/dropped domain (drop-catching)')
168
+ .option('--years <n>', 'Registration years', '1')
169
+ .option('--privacy', 'Enable WHOIS privacy', false)
170
+ .option('--no-auto-renew', 'Disable auto-renewal')
171
+ .action(async (domain, opts) => {
172
+ try {
173
+ const result = await dns.buyDrop(domain, {
174
+ years: parseInt(opts.years),
175
+ privacy: opts.privacy,
176
+ autoRenew: opts.autoRenew !== false
177
+ });
178
+ tick(`Drop registration initiated for ${domain}`);
179
+ output(result, (r) => {
180
+ if (r.order_amount) console.log(` Order amount: $${r.order_amount}`);
181
+ });
182
+ } catch (err) { handleError(err); }
183
+ });
184
+
185
+ // ─── list ─────────────────────────────────────────────────────────────────────
186
+
187
+ program
188
+ .command('list')
189
+ .description('List all your domains')
190
+ .action(async () => {
191
+ try {
192
+ const domains = await dns.list();
193
+ output(domains, (data) => {
194
+ if (!data.length) { console.log('No domains found.'); return; }
195
+ const table = new Table({
196
+ head: ['Domain', 'Status', 'Expires', 'Auto-Renew', 'Locked'],
197
+ style: { head: ['cyan'] }
198
+ });
199
+ data.forEach(d => {
200
+ table.push([
201
+ d.fqdn,
202
+ d.status || 'active',
203
+ d.expires_at ? new Date(d.expires_at).toDateString() : '—',
204
+ d.auto_renew ? 'on' : 'off',
205
+ d.locked ? 'yes' : 'no'
206
+ ]);
207
+ });
208
+ console.log(table.toString());
209
+ console.log(`\nTotal: ${data.length} domain(s)`);
210
+ });
211
+ } catch (err) { handleError(err); }
212
+ });
213
+
214
+ // ─── info ─────────────────────────────────────────────────────────────────────
215
+
216
+ program
217
+ .command('info <domain>')
218
+ .description('Get full domain details')
219
+ .action(async (domain) => {
220
+ try {
221
+ const info = await dns.info(domain);
222
+ output(info, (d) => {
223
+ console.log(`\nDomain: ${d.fqdn}`);
224
+ console.log(` Status: ${d.status}`);
225
+ console.log(` Registered: ${d.registered_at ? new Date(d.registered_at).toDateString() : '—'}`);
226
+ console.log(` Expires: ${d.expires_at ? new Date(d.expires_at).toDateString() : '—'}`);
227
+ console.log(` Auto-Renew: ${d.auto_renew ? 'on' : 'off'}`);
228
+ console.log(` Nameservers: ${(d.nameservers || []).join(', ')}`);
229
+ });
230
+ } catch (err) { handleError(err); }
231
+ });
232
+
233
+ // ─── records ─────────────────────────────────────────────────────────────────
234
+
235
+ const recordsCmd = program.command('records').description('DNS record management');
236
+
237
+ recordsCmd
238
+ .command('list <domain>')
239
+ .description('List DNS records for a domain')
240
+ .option('--type <type>', 'Filter by record type (A, CNAME, MX, TXT...)')
241
+ .action(async (domain, opts) => {
242
+ try {
243
+ let recs = await dns.records.list(domain);
244
+ if (opts.type) recs = recs.filter(r => r.type === opts.type.toUpperCase());
245
+ output(recs, (data) => {
246
+ if (!data.length) { console.log('No records found.'); return; }
247
+ const table = new Table({
248
+ head: ['ID', 'Type', 'Name', 'Value', 'TTL', 'Priority'],
249
+ style: { head: ['cyan'] }
250
+ });
251
+ data.forEach(r => {
252
+ table.push([
253
+ r.id.slice(0, 8) + '...',
254
+ r.type,
255
+ r.name,
256
+ r.value.length > 40 ? r.value.slice(0, 37) + '...' : r.value,
257
+ r.ttl,
258
+ r.priority || '—'
259
+ ]);
260
+ });
261
+ console.log(table.toString());
262
+ console.log(`\nTotal: ${data.length} record(s)`);
263
+ });
264
+ } catch (err) { handleError(err); }
265
+ });
266
+
267
+ recordsCmd
268
+ .command('add <domain> <type> <name> <value>')
269
+ .description('Add a DNS record (e.g.: records add cool.io A @ 1.2.3.4)')
270
+ .option('--ttl <seconds>', 'TTL in seconds', '300')
271
+ .option('--priority <n>', 'Priority (for MX/SRV records)')
272
+ .option('--file <path>', 'Bulk import from JSON file')
273
+ .action(async (domain, type, name, value, opts) => {
274
+ try {
275
+ if (opts.file) {
276
+ const fs = require('fs');
277
+ const records = JSON.parse(fs.readFileSync(opts.file, 'utf8'));
278
+ const results = await dns.importRecords(domain, records);
279
+ tick(`Imported ${results.length} records to ${domain}`);
280
+ if (isJson()) console.log(JSON.stringify(results, null, 2));
281
+ return;
282
+ }
283
+ const record = await dns.records.add(domain, {
284
+ type, name, value,
285
+ ttl: parseInt(opts.ttl),
286
+ priority: opts.priority ? parseInt(opts.priority) : undefined
287
+ });
288
+ tick(`${type} record added to ${domain}`);
289
+ output(record, (r) => {
290
+ console.log(` ID: ${r.id}`);
291
+ console.log(` Type: ${r.type}`);
292
+ console.log(` Name: ${r.name}`);
293
+ console.log(` Value: ${r.value}`);
294
+ console.log(` TTL: ${r.ttl}s`);
295
+ });
296
+ } catch (err) { handleError(err); }
297
+ });
298
+
299
+ recordsCmd
300
+ .command('update <domain> <id>')
301
+ .description('Update a DNS record by ID')
302
+ .option('--type <type>', 'Record type')
303
+ .option('--name <name>', 'Record name/host')
304
+ .option('--value <value>', 'Record value')
305
+ .option('--ttl <seconds>', 'TTL in seconds')
306
+ .option('--priority <n>', 'Priority')
307
+ .action(async (domain, id, opts) => {
308
+ try {
309
+ const record = await dns.records.update(domain, id, {
310
+ type: opts.type,
311
+ name: opts.name,
312
+ value: opts.value,
313
+ ttl: opts.ttl ? parseInt(opts.ttl) : undefined,
314
+ priority: opts.priority ? parseInt(opts.priority) : undefined
315
+ });
316
+ tick(`Record ${id.slice(0,8)}... updated`);
317
+ output(record, (r) => {
318
+ console.log(` ID: ${r.id}`);
319
+ console.log(` Type: ${r.type}`);
320
+ console.log(` Name: ${r.name}`);
321
+ console.log(` Value: ${r.value}`);
322
+ console.log(` TTL: ${r.ttl}s`);
323
+ });
324
+ } catch (err) { handleError(err); }
325
+ });
326
+
327
+ recordsCmd
328
+ .command('delete <domain> <id>')
329
+ .description('Delete a DNS record by ID')
330
+ .action(async (domain, id) => {
331
+ try {
332
+ await dns.records.delete(domain, id);
333
+ tick(`Record ${id.slice(0,8)}... deleted from ${domain}`);
334
+ } catch (err) { handleError(err); }
335
+ });
336
+
337
+ recordsCmd
338
+ .command('export <domain>')
339
+ .description('Export all DNS records as JSON (pipe to file: saac_dns records export cool.io > records.json)')
340
+ .option('--format <fmt>', 'Output format: json (default)', 'json')
341
+ .option('--out <file>', 'Write output to file instead of stdout')
342
+ .action(async (domain, opts) => {
343
+ try {
344
+ const records = await dns.exportRecords(domain);
345
+ const json = JSON.stringify(records, null, 2);
346
+ if (opts.out) {
347
+ const fs = require('fs');
348
+ fs.writeFileSync(opts.out, json, 'utf8');
349
+ tick(`${records.length} record(s) exported to ${opts.out}`);
350
+ } else {
351
+ // Always output raw JSON for export (suitable for piping)
352
+ console.log(json);
353
+ if (!isJson()) console.error(` ✓ ${records.length} record(s) from ${domain}`);
354
+ }
355
+ } catch (err) { handleError(err); }
356
+ });
357
+
358
+ // ─── ns (nameservers) ─────────────────────────────────────────────────────────
359
+
360
+ const nsCmd = program.command('ns').description('Nameserver management');
361
+
362
+ nsCmd
363
+ .command('set <domain> <nameservers...>')
364
+ .description('Set nameservers for a domain')
365
+ .action(async (domain, nameserverList) => {
366
+ try {
367
+ const result = await dns.nameservers.set(domain, nameserverList);
368
+ tick(`Nameservers updated for ${domain}`);
369
+ output(result, () => {
370
+ console.log(` Nameservers: ${nameserverList.join(', ')}`);
371
+ });
372
+ } catch (err) { handleError(err); }
373
+ });
374
+
375
+ nsCmd
376
+ .command('list <domain>')
377
+ .description('List nameservers for a domain')
378
+ .action(async (domain) => {
379
+ try {
380
+ const ns = await dns.nameservers.list(domain);
381
+ output(ns, (data) => {
382
+ console.log(`Nameservers for ${domain}:`);
383
+ data.forEach((n, i) => console.log(` ${i + 1}. ${n}`));
384
+ });
385
+ } catch (err) { handleError(err); }
386
+ });
387
+
388
+ nsCmd
389
+ .command('register <domain> <host> <ips...>')
390
+ .description('Register a private nameserver (e.g. ns register example.com ns1.example.com 1.2.3.4)')
391
+ .action(async (domain, host, ips) => {
392
+ try {
393
+ const result = await dns.nameservers.register(domain, host, ips);
394
+ tick(`Nameserver ${host} registered for ${domain}`);
395
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
396
+ } catch (err) { handleError(err); }
397
+ });
398
+
399
+ nsCmd
400
+ .command('modify <domain> <host> <ips...>')
401
+ .description('Modify a registered private nameserver IPs')
402
+ .action(async (domain, host, ips) => {
403
+ try {
404
+ const result = await dns.nameservers.modify(domain, host, ips);
405
+ tick(`Nameserver ${host} updated`);
406
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
407
+ } catch (err) { handleError(err); }
408
+ });
409
+
410
+ nsCmd
411
+ .command('unregister <domain> <host>')
412
+ .description('Unregister/delete a private nameserver')
413
+ .action(async (domain, host) => {
414
+ try {
415
+ await dns.nameservers.unregister(domain, host);
416
+ tick(`Nameserver ${host} unregistered from ${domain}`);
417
+ } catch (err) { handleError(err); }
418
+ });
419
+
420
+ nsCmd
421
+ .command('list-registered <domain>')
422
+ .description('List registered private nameservers for a domain')
423
+ .action(async (domain) => {
424
+ try {
425
+ const hosts = await dns.nameservers.listRegistered(domain);
426
+ output(hosts, (data) => {
427
+ if (!data.length) { console.log('No registered nameservers.'); return; }
428
+ data.forEach((h, i) => console.log(` ${i + 1}. ${h.host || h} — ${h.ip || ''}`));
429
+ });
430
+ } catch (err) { handleError(err); }
431
+ });
432
+
433
+ // ─── whois ────────────────────────────────────────────────────────────────────
434
+
435
+ program
436
+ .command('whois <domain>')
437
+ .description('WHOIS lookup for a domain')
438
+ .action(async (domain) => {
439
+ try {
440
+ const info = await dns.whois(domain);
441
+ output(info, (d) => {
442
+ console.log(`\nWHOIS: ${domain}`);
443
+ console.log(` Registrar: ${d.registrar || '—'}`);
444
+ console.log(` Status: ${d.status || '—'}`);
445
+ console.log(` Created: ${d.created || d.registered_at || '—'}`);
446
+ console.log(` Updated: ${d.updated || '—'}`);
447
+ console.log(` Expires: ${d.expires || d.expires_at || '—'}`);
448
+ console.log(` Nameservers:`);
449
+ const ns = d.nameservers ? (d.nameservers.nameserver || d.nameservers) : [];
450
+ (Array.isArray(ns) ? ns : [ns]).forEach(n => console.log(` - ${n}`));
451
+ });
452
+ } catch (err) { handleError(err); }
453
+ });
454
+ // ─── prices ──────────────────────────────────────────────────────────────────
455
+
456
+ program
457
+ .command('prices')
458
+ .description('Show domain registration prices')
459
+ .option('--tld <tlds>', 'Comma-separated TLDs (e.g. com,io,dev)')
460
+ .action(async (opts) => {
461
+ try {
462
+ const tlds = opts.tld ? opts.tld.split(',').map(t => t.startsWith('.') ? t : `.${t}`) : null;
463
+ const priceData = await dns.prices(tlds);
464
+ output(priceData, (data) => {
465
+ const table = new Table({
466
+ head: ['TLD', 'Price/yr'],
467
+ style: { head: ['cyan'] }
468
+ });
469
+ Object.entries(data).forEach(([tld, price]) => {
470
+ table.push([tld, `$${price}`]);
471
+ });
472
+ console.log(table.toString());
473
+ });
474
+ } catch (err) { handleError(err); }
475
+ });
476
+
477
+ // ─── lock ────────────────────────────────────────────────────────────────────
478
+
479
+ program
480
+ .command('lock <domain>')
481
+ .description('Lock a domain to prevent unauthorized transfers')
482
+ .action(async (domain) => {
483
+ try {
484
+ const result = await dns.lock(domain);
485
+ tick(`Domain ${domain} locked`);
486
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
487
+ } catch (err) { handleError(err); }
488
+ });
489
+
490
+ // ─── unlock ──────────────────────────────────────────────────────────────────
491
+
492
+ program
493
+ .command('unlock <domain>')
494
+ .description('Unlock a domain (e.g. before initiating a transfer)')
495
+ .action(async (domain) => {
496
+ try {
497
+ const result = await dns.unlock(domain);
498
+ tick(`Domain ${domain} unlocked`);
499
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
500
+ } catch (err) { handleError(err); }
501
+ });
502
+
503
+ // ─── auto-renew ──────────────────────────────────────────────────────────────
504
+
505
+ program
506
+ .command('auto-renew <domain> <on|off>')
507
+ .description('Toggle auto-renewal for a domain')
508
+ .action(async (domain, toggle) => {
509
+ try {
510
+ const enabled = toggle === 'on';
511
+ const result = await dns.autoRenew(domain, enabled);
512
+ tick(`Auto-renewal ${enabled ? 'enabled' : 'disabled'} for ${domain}`);
513
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
514
+ } catch (err) { handleError(err); }
515
+ });
516
+
517
+ // ─── privacy ─────────────────────────────────────────────────────────────────
518
+
519
+ program
520
+ .command('privacy <domain> <on|off>')
521
+ .description('Toggle WHOIS privacy for a domain')
522
+ .action(async (domain, toggle) => {
523
+ try {
524
+ const enabled = toggle === 'on';
525
+ const result = await dns.privacy(domain, enabled);
526
+ tick(`WHOIS privacy ${enabled ? 'enabled' : 'disabled'} for ${domain}`);
527
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
528
+ } catch (err) { handleError(err); }
529
+ });
530
+
531
+ // ─── renew ───────────────────────────────────────────────────────────────────
532
+
533
+ program
534
+ .command('renew <domain>')
535
+ .description('Renew a domain')
536
+ .option('--years <n>', 'Renewal years', '1')
537
+ .action(async (domain, opts) => {
538
+ try {
539
+ const result = await dns.renew(domain, parseInt(opts.years));
540
+ tick(`Domain ${domain} renewed for ${opts.years} year(s)`);
541
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
542
+ } catch (err) { handleError(err); }
543
+ });
544
+
545
+ // ─── account ─────────────────────────────────────────────────────────────────
546
+
547
+ const accountCmd = program.command('account').description('Account management');
548
+
549
+ accountCmd
550
+ .command('balance')
551
+ .description('Check account balance')
552
+ .option('--warn-below <amount>', 'Show warning if balance is below this amount (default: 20)', '20')
553
+ .action(async (opts) => {
554
+ try {
555
+ const data = await dns.account.balance();
556
+ output(data, (d) => {
557
+ const bal = parseFloat(d.balance || '0');
558
+ const warnThreshold = parseFloat(opts.warnBelow) || 20;
559
+ console.log(`\nAccount Balance: $${bal.toFixed(2)} USD`);
560
+ if (bal < warnThreshold) {
561
+ console.warn(`\nWARNING: Balance ($${bal.toFixed(2)}) is below warning threshold ($${warnThreshold.toFixed(2)}). Please top up your account.`);
562
+ }
563
+ });
564
+ } catch (err) { handleError(err); }
565
+ });
566
+
567
+ accountCmd
568
+ .command('fund <amount>')
569
+ .description('Add funds to your account (requires a saved payment method ID)')
570
+ .option('--payment-id <id>', 'Saved payment method ID')
571
+ .action(async (amount, opts) => {
572
+ try {
573
+ if (!opts.paymentId) {
574
+ console.error('✗ --payment-id is required. Run: saac_dns account payment-methods');
575
+ process.exit(1);
576
+ }
577
+ const result = await dns.account.addFunds(parseFloat(amount), opts.paymentId);
578
+ tick(`$${amount} added to account`);
579
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
580
+ } catch (err) { handleError(err); }
581
+ });
582
+
583
+ accountCmd
584
+ .command('payment-methods')
585
+ .description('List saved payment methods')
586
+ .action(async () => {
587
+ try {
588
+ const methods = await dns.account.listPaymentMethods();
589
+ output(methods, (data) => {
590
+ if (!data.length) { console.log('No saved payment methods.'); return; }
591
+ const table = new Table({
592
+ head: ['ID', 'Type', 'Last 4', 'Expiry'],
593
+ style: { head: ['cyan'] }
594
+ });
595
+ data.forEach(m => table.push([m.id || m.payment_id || '—', m.type || '—', m.last4 || '—', m.expiry || '—']));
596
+ console.log(table.toString());
597
+ });
598
+ } catch (err) { handleError(err); }
599
+ });
600
+
601
+ // ─── config ──────────────────────────────────────────────────────────────────
602
+
603
+ program
604
+ .command('config')
605
+ .description('View or set default preferences')
606
+ .option('--privacy <on|off>', 'Default WHOIS privacy')
607
+ .option('--auto-renew <on|off>', 'Default auto-renewal')
608
+ .option('--ttl <seconds>', 'Default TTL for DNS records')
609
+ .option('--require-purchase-confirmation <on|off>', 'Require email confirmation before domain purchase (set off for agent/automation use)')
610
+ .action(async (opts) => {
611
+ try {
612
+ const me = await dns.account.me();
613
+ if (!opts.privacy && !opts.autoRenew && !opts.ttl && !opts.requirePurchaseConfirmation) {
614
+ output(me, (d) => {
615
+ console.log('\nCurrent config:');
616
+ console.log(` Email: ${d.email}`);
617
+ console.log(` API Key: ${d.api_key ? d.api_key.slice(0,10) + '...' : '—'}`);
618
+ console.log(` Domains: ${d.stats?.domains || 0}`);
619
+ console.log('\nEnvironment:');
620
+ console.log(` SAAC_USER_API_KEY: ${process.env.SAAC_USER_API_KEY ? '✓ Set' : '✗ Not set'}`);
621
+ console.log(` SAAC_USER_EMAIL: ${process.env.SAAC_USER_EMAIL ? '✓ Set' : '✗ Not set'}`);
622
+ });
623
+ } else {
624
+ const prefs = {};
625
+ if (opts.privacy) prefs.default_privacy = opts.privacy === 'on';
626
+ if (opts.autoRenew) prefs.default_auto_renew = opts.autoRenew === 'on';
627
+ if (opts.ttl) prefs.default_ttl = parseInt(opts.ttl);
628
+ if (opts.requirePurchaseConfirmation) prefs.require_purchase_confirmation = opts.requirePurchaseConfirmation === 'on';
629
+ await dns.account.preferences(prefs);
630
+ tick('Preferences updated');
631
+ }
632
+ } catch (err) { handleError(err); }
633
+ });
634
+
635
+ // ─── forward ─────────────────────────────────────────────────────────────────
636
+
637
+ program
638
+ .command('forward <domain> <url>')
639
+ .description('Set up domain forwarding')
640
+ .action(async (domain, url) => {
641
+ try {
642
+ const result = await dns.forwarding.set(domain, url);
643
+ output(result, () => tick(`${domain} now forwards to ${url}`));
644
+ } catch (err) { handleError(err); }
645
+ });
646
+
647
+ program
648
+ .command('forward-sub <subdomain> <url>')
649
+ .description('Set up subdomain forwarding (e.g. forward-sub blog.cool.io https://myblog.com)')
650
+ .action(async (subdomain, url) => {
651
+ try {
652
+ const parts = subdomain.split('.');
653
+ const sub = parts[0];
654
+ const domain = parts.slice(1).join('.');
655
+ const result = await dns.forwarding.setSub(domain, sub, url);
656
+ output(result, () => tick(`${subdomain} now forwards to ${url}`));
657
+ } catch (err) { handleError(err); }
658
+ });
659
+
660
+ // ─── contacts ────────────────────────────────────────────────────────────────
661
+
662
+ const contactsCmd = program.command('contacts').description('WHOIS contact management');
663
+
664
+ contactsCmd
665
+ .command('list')
666
+ .description('List contact profiles')
667
+ .action(async () => {
668
+ try {
669
+ const contacts = await dns.contacts.list();
670
+ output(contacts, (data) => {
671
+ if (!data.length) { console.log('No contacts found.'); return; }
672
+ const table = new Table({
673
+ head: ['ID', 'Name', 'Email', 'Country'],
674
+ style: { head: ['cyan'] }
675
+ });
676
+ data.forEach(c => {
677
+ table.push([c.id.slice(0,8)+'...', `${c.first_name} ${c.last_name}`, c.email || '—', c.country || '—']);
678
+ });
679
+ console.log(table.toString());
680
+ });
681
+ } catch (err) { handleError(err); }
682
+ });
683
+
684
+ contactsCmd
685
+ .command('add')
686
+ .description('Add a new WHOIS contact')
687
+ .option('--interactive', 'Interactive guided mode', false)
688
+ .option('--first-name <n>', 'First name')
689
+ .option('--last-name <n>', 'Last name')
690
+ .option('--email <e>', 'Email address')
691
+ .option('--org <o>', 'Organization')
692
+ .option('--address <a>', 'Street address')
693
+ .option('--city <c>', 'City')
694
+ .option('--state <s>', 'State/Province')
695
+ .option('--zip <z>', 'ZIP/Postal code')
696
+ .option('--country <cc>', 'Country code (e.g. US)')
697
+ .option('--phone <p>', 'Phone number (e.g. +1.3125551234)')
698
+ .action(async (opts) => {
699
+ try {
700
+ let contact = {};
701
+
702
+ if (opts.interactive) {
703
+ const readline = require('readline');
704
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
705
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve));
706
+
707
+ console.log('\nAdd Contact — Interactive Mode\n');
708
+ contact.first_name = await ask('First name: ');
709
+ contact.last_name = await ask('Last name: ');
710
+ contact.email = await ask('Email: ');
711
+ contact.org = await ask('Organization (optional): ');
712
+ contact.address = await ask('Street address: ');
713
+ contact.city = await ask('City: ');
714
+ contact.state = await ask('State/Province: ');
715
+ contact.zip = await ask('ZIP/Postal code: ');
716
+ contact.country = await ask('Country code (e.g. US): ');
717
+ contact.phone = await ask('Phone (e.g. +1.3125551234): ');
718
+ rl.close();
719
+ console.log('');
720
+ } else {
721
+ contact = {
722
+ first_name: opts.firstName,
723
+ last_name: opts.lastName,
724
+ email: opts.email,
725
+ org: opts.org,
726
+ address: opts.address,
727
+ city: opts.city,
728
+ state: opts.state,
729
+ zip: opts.zip,
730
+ country: opts.country,
731
+ phone: opts.phone
732
+ };
733
+ }
734
+
735
+ const result = await dns.contacts.add(contact);
736
+ tick(`Contact added (ID: ${result.id || result.contact_id || '—'})`);
737
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
738
+ } catch (err) { handleError(err); }
739
+ });
740
+
741
+ contactsCmd
742
+ .command('set <domain>')
743
+ .description('Assign a contact to a domain')
744
+ .option('--contact <id>', 'Contact ID')
745
+ .action(async (domain, opts) => {
746
+ try {
747
+ if (!opts.contact) { handleError(new Error('--contact <id> is required')); return; }
748
+ await dns.contacts.associate(opts.contact, domain);
749
+ tick(`Contact ${opts.contact.slice(0,8)}... assigned to ${domain}`);
750
+ } catch (err) { handleError(err); }
751
+ });
752
+
753
+
754
+ contactsCmd
755
+ .command('update <id>')
756
+ .description('Update a contact profile')
757
+ .option('--first-name <name>', 'First name')
758
+ .option('--last-name <name>', 'Last name')
759
+ .option('--email <email>', 'Email address')
760
+ .option('--org <org>', 'Organization')
761
+ .option('--address <addr>', 'Street address')
762
+ .option('--city <city>', 'City')
763
+ .option('--state <state>', 'State/Province')
764
+ .option('--zip <zip>', 'ZIP/Postal code')
765
+ .option('--country <cc>', 'Country code (e.g. US)')
766
+ .option('--phone <phone>', 'Phone number')
767
+ .action(async (id, opts) => {
768
+ try {
769
+ const contact = await dns.contacts.update(id, {
770
+ first_name: opts.firstName, last_name: opts.lastName,
771
+ email: opts.email, org: opts.org, address: opts.address,
772
+ city: opts.city, state: opts.state, zip: opts.zip,
773
+ country: opts.country, phone: opts.phone
774
+ });
775
+ tick(`Contact ${id.slice(0,8)}... updated`);
776
+ output(contact, (c) => { console.log(` ID: ${c.id}`); });
777
+ } catch (err) { handleError(err); }
778
+ });
779
+
780
+ contactsCmd
781
+ .command('delete <id>')
782
+ .description('Delete a contact profile')
783
+ .action(async (id) => {
784
+ try {
785
+ await dns.contacts.delete(id);
786
+ tick(`Contact ${id.slice(0,8)}... deleted`);
787
+ } catch (err) { handleError(err); }
788
+ });
789
+
790
+ // ─── Domain Transfer Suite ───────────────────────────────────────────────────
791
+
792
+ program
793
+ .command('transfer <domain>')
794
+ .description('Initiate a domain transfer in')
795
+ .option('--auth <code>', 'EPP/auth code from current registrar')
796
+ .option('--years <n>', 'Transfer period in years', '1')
797
+ .action(async (domain, opts) => {
798
+ try {
799
+ if (!opts.auth) { handleError(new Error('--auth <code> is required')); return; }
800
+ const result = await dns.transfer(domain, opts.auth, { years: parseInt(opts.years) });
801
+ tick(`Transfer initiated for ${domain}`);
802
+ output(result, () => console.log(` Status: Transfer in progress`));
803
+ } catch (err) { handleError(err); }
804
+ });
805
+
806
+ program
807
+ .command('transfer-check <domain>')
808
+ .description('Check if a domain is transferable')
809
+ .action(async (domain) => {
810
+ try {
811
+ const result = await dns.transferCheck(domain);
812
+ output(result, (d) => {
813
+ console.log(`Domain: ${domain}`);
814
+ console.log(`Transferable: ${d.transferable || d.code === 300 ? 'Yes' : 'No'}`);
815
+ });
816
+ } catch (err) { handleError(err); }
817
+ });
818
+
819
+ program
820
+ .command('transfer-status <domain>')
821
+ .description('Check the status of an in-progress domain transfer')
822
+ .action(async (domain) => {
823
+ try {
824
+ const result = await dns.transferStatus(domain);
825
+ output(result, (d) => {
826
+ console.log(`Domain: ${domain}`);
827
+ console.log(`Status: ${d.status || '(see JSON output)'}`);
828
+ if (d.step) console.log(`Step: ${d.step}`);
829
+ if (d.message) console.log(`Message: ${d.message}`);
830
+ });
831
+ } catch (err) { handleError(err); }
832
+ });
833
+
834
+ // ─── transfer-update-epp ─────────────────────────────────────────────────────
835
+ program
836
+ .command('transfer-update-epp <domain>')
837
+ .description('Update EPP/auth code for an in-progress transfer')
838
+ .option('--auth <code>', 'New EPP/auth code')
839
+ .action(async (domain, opts) => {
840
+ try {
841
+ if (!opts.auth) { console.error('✗ --auth <code> is required'); process.exit(1); }
842
+ const result = await dns.transferUpdateEpp(domain, opts.auth);
843
+ tick(`EPP code updated for ${domain} transfer`);
844
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
845
+ } catch (err) { handleError(err); }
846
+ });
847
+
848
+ // ─── transfer-resend ──────────────────────────────────────────────────────────
849
+ program
850
+ .command('transfer-resend <domain>')
851
+ .description('Resend admin approval email for an in-progress transfer')
852
+ .action(async (domain) => {
853
+ try {
854
+ const result = await dns.transferResendEmail(domain);
855
+ tick(`Admin approval email resent for ${domain}`);
856
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
857
+ } catch (err) { handleError(err); }
858
+ });
859
+
860
+ // ─── transfer-resubmit ────────────────────────────────────────────────────────
861
+ program
862
+ .command('transfer-resubmit <domain>')
863
+ .description('Resubmit a transfer to the registry')
864
+ .action(async (domain) => {
865
+ try {
866
+ const result = await dns.transferResubmit(domain);
867
+ tick(`Transfer resubmitted for ${domain}`);
868
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
869
+ } catch (err) { handleError(err); }
870
+ });
871
+
872
+ program
873
+ .command('auth-code <domain>')
874
+ .description('Retrieve EPP/auth code for outbound transfer')
875
+ .action(async (domain) => {
876
+ try {
877
+ const result = await dns.authCode(domain);
878
+ output(result, (d) => {
879
+ console.log(`Domain: ${domain}`);
880
+ console.log(`Auth Code: ${d.auth_code || d.code || '(see JSON output)'}`);
881
+ });
882
+ } catch (err) { handleError(err); }
883
+ });
884
+
885
+
886
+ // ─── expiring ─────────────────────────────────────────────────────────────────
887
+
888
+ program
889
+ .command('expiring')
890
+ .description('List domains expiring soon')
891
+ .option('--days <n>', 'Days ahead to check', '30')
892
+ .action(async (opts) => {
893
+ try {
894
+ const domains = await dns.expiring(parseInt(opts.days));
895
+ output(domains, (data) => {
896
+ if (!data.length) { console.log(`No domains expiring in the next ${opts.days} days.`); return; }
897
+ const table = new Table({
898
+ head: ['Domain', 'Expires At', 'Auto Renew'],
899
+ style: { head: ['cyan'] }
900
+ });
901
+ data.forEach(d => {
902
+ table.push([d.fqdn, d.expires_at ? new Date(d.expires_at).toLocaleDateString() : '—', d.auto_renew ? 'Yes' : 'No']);
903
+ });
904
+ console.log(table.toString());
905
+ });
906
+ } catch (err) { handleError(err); }
907
+ });
908
+
909
+ // ─── key management ───────────────────────────────────────────────────────────────────────────
910
+
911
+ const keyCmd = program.command('key').description('API key and security management');
912
+
913
+ keyCmd
914
+ .command('rotate')
915
+ .description('Rotate your SAAC API key')
916
+ .action(async () => {
917
+ try {
918
+ const result = await dns.account.rotateKey();
919
+ tick('API key rotated');
920
+ output(result, () => {
921
+ console.log(` New API key: ${result.api_key}`);
922
+ console.log(` Update SAAC_USER_API_KEY in your environment.`);
923
+ });
924
+ } catch (err) { handleError(err); }
925
+ });
926
+
927
+ const allowlistCmd = keyCmd.command('allowlist').description('IP allowlist management');
928
+
929
+ allowlistCmd
930
+ .command('list')
931
+ .description('List allowed IPs')
932
+ .action(async () => {
933
+ try {
934
+ const result = await dns.account.allowlist.list();
935
+ output(result, (data) => {
936
+ if (!data.ip_allowlist || !data.ip_allowlist.length) {
937
+ console.log('No IP restrictions set (all IPs allowed)');
938
+ } else {
939
+ data.ip_allowlist.forEach((ip, i) => console.log(` ${i + 1}. ${ip}`));
940
+ }
941
+ });
942
+ } catch (err) { handleError(err); }
943
+ });
944
+
945
+ allowlistCmd
946
+ .command('add <ip>')
947
+ .description('Add IP or CIDR to allowlist')
948
+ .action(async (ip) => {
949
+ try {
950
+ const result = await dns.account.allowlist.add(ip);
951
+ tick(`${ip} added to allowlist`);
952
+ output(result, (data) => console.log(` Total IPs: ${data.ip_allowlist.length}`));
953
+ } catch (err) { handleError(err); }
954
+ });
955
+
956
+ allowlistCmd
957
+ .command('remove <ip>')
958
+ .description('Remove IP or CIDR from allowlist')
959
+ .action(async (ip) => {
960
+ try {
961
+ const result = await dns.account.allowlist.remove(ip);
962
+ tick(`${ip} removed from allowlist`);
963
+ output(result, (data) => console.log(` Remaining IPs: ${data.ip_allowlist.length}`));
964
+ } catch (err) { handleError(err); }
965
+ });
966
+
967
+ // ─── setup-web ───────────────────────────────────────────────────────────────
968
+
969
+ program
970
+ .command('setup-web <domain>')
971
+ .description('Set up web DNS records (A + CNAME). Use --hosting for provider templates.')
972
+ .option('--ip <address>', 'IP address for A record')
973
+ .option('--hosting <provider>', 'Hosting provider: vercel, netlify, cloudflare, railway, render')
974
+ .action(async (domain, opts) => {
975
+ try {
976
+ const target = opts.hosting || opts.ip;
977
+ if (!target) {
978
+ console.error('✗ Provide --hosting <provider> or --ip <address>');
979
+ console.error(' Providers: vercel, netlify, cloudflare, railway, render');
980
+ process.exit(1);
981
+ }
982
+ const result = await dns.setupWeb(domain, target);
983
+ tick(result.message);
984
+ if (result.note) console.log(` Note: ${result.note}`);
985
+ output(result, (r) => {
986
+ console.log(` Records added: ${r.records ? r.records.length : 0}`);
987
+ });
988
+ } catch (err) { handleError(err); }
989
+ });
990
+
991
+ // ─── setup-email ─────────────────────────────────────────────────────────────
992
+
993
+ program
994
+ .command('setup-email <domain> [provider]')
995
+ .description('Set up email DNS records (MX + SPF + DMARC) for an email provider')
996
+ .option('--provider <name>', 'Email provider: gmail, office365, microsoft365, zoho, titan', 'gmail')
997
+ .action(async (domain, providerArg, opts) => {
998
+ try {
999
+ // Support both positional arg (setup-email domain.com titan) and --provider flag
1000
+ const provider = providerArg || opts.provider || 'gmail';
1001
+ const result = await dns.setupEmail(domain, provider);
1002
+ tick(result.message);
1003
+ if (result.note && !isJson()) console.log(` Next step: ${result.note}`);
1004
+ output(result, (r) => {
1005
+ console.log(` Records added: ${r.records ? r.records.length : 0}`);
1006
+ if (r.note) console.log(` Note: ${r.note}`);
1007
+ });
1008
+ } catch (err) { handleError(err); }
1009
+ });
1010
+
1011
+ // ─── clone-dns ───────────────────────────────────────────────────────────────
1012
+
1013
+ program
1014
+ .command('clone-dns <source> <target>')
1015
+ .description('Clone all DNS records from one domain to another')
1016
+ .action(async (source, target) => {
1017
+ try {
1018
+ const result = await dns.cloneDns(source, target);
1019
+ tick(result.message);
1020
+ output(result, (r) => {
1021
+ console.log(` Cloned: ${r.cloned}`);
1022
+ if (r.failed) console.log(` Failed: ${r.failed}`);
1023
+ });
1024
+ } catch (err) { handleError(err); }
1025
+ });
1026
+
1027
+ // ─── email-fwd ───────────────────────────────────────────────────────────────
1028
+
1029
+ const emailFwdCmd = program.command('email-fwd').description('Email forwarding management');
1030
+
1031
+ emailFwdCmd
1032
+ .command('list <domain>')
1033
+ .description('List email forwards for a domain')
1034
+ .action(async (domain) => {
1035
+ try {
1036
+ const forwards = await dns.email.list(domain);
1037
+ output(forwards, (data) => {
1038
+ if (!data.length) { console.log('No email forwards configured.'); return; }
1039
+ const table = new Table({
1040
+ head: ['From', 'To'],
1041
+ style: { head: ['cyan'] }
1042
+ });
1043
+ data.forEach(f => table.push([f.from_email || f.email2 || '—', f.to_email || f.forward2 || '—']));
1044
+ console.log(table.toString());
1045
+ });
1046
+ } catch (err) { handleError(err); }
1047
+ });
1048
+
1049
+ emailFwdCmd
1050
+ .command('add <domain> <from> <to>')
1051
+ .description('Add email forward: <from>@domain → <to>')
1052
+ .action(async (domain, from, to) => {
1053
+ try {
1054
+ const result = await dns.email.forward(domain, from, to);
1055
+ tick(`Email forward ${from}@${domain} → ${to} configured`);
1056
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
1057
+ } catch (err) { handleError(err); }
1058
+ });
1059
+
1060
+ emailFwdCmd
1061
+ .command('delete <domain> <from>')
1062
+ .description('Delete email forward for <from>@domain')
1063
+ .action(async (domain, from) => {
1064
+ try {
1065
+ await dns.email.deleteForward(domain, from);
1066
+ tick(`Email forward ${from}@${domain} deleted`);
1067
+ } catch (err) { handleError(err); }
1068
+ });
1069
+
1070
+ // ─── dnssec ──────────────────────────────────────────────────────────────────
1071
+
1072
+ const dnssecCmd = program.command('dnssec').description('DNSSEC management');
1073
+
1074
+ dnssecCmd
1075
+ .command('list <domain>')
1076
+ .description('List DNSSEC records for a domain')
1077
+ .action(async (domain) => {
1078
+ try {
1079
+ const records = await dns.dnssec.list(domain);
1080
+ output(records, (data) => {
1081
+ if (!data.length) { console.log('No DNSSEC records configured.'); return; }
1082
+ const table = new Table({
1083
+ head: ['Algorithm', 'Key Tag', 'Digest Type', 'Digest'],
1084
+ style: { head: ['cyan'] }
1085
+ });
1086
+ data.forEach(r => table.push([r.algorithm || '—', r.key_tag || '—', r.digest_type || '—', (r.digest || '—').slice(0, 20) + '...']));
1087
+ console.log(table.toString());
1088
+ });
1089
+ } catch (err) { handleError(err); }
1090
+ });
1091
+
1092
+ dnssecCmd
1093
+ .command('add <domain>')
1094
+ .description('Add a DNSSEC DS record')
1095
+ .option('--algorithm <n>', 'Algorithm number')
1096
+ .option('--key-tag <n>', 'Key tag number')
1097
+ .option('--digest-type <n>', 'Digest type number')
1098
+ .option('--digest <hash>', 'Digest hash value')
1099
+ .action(async (domain, opts) => {
1100
+ try {
1101
+ if (!opts.algorithm || !opts.keyTag || !opts.digestType || !opts.digest) {
1102
+ console.error('✗ All of --algorithm, --key-tag, --digest-type, --digest are required');
1103
+ process.exit(1);
1104
+ }
1105
+ const result = await dns.dnssec.add(domain, {
1106
+ algorithm: opts.algorithm,
1107
+ key_tag: opts.keyTag,
1108
+ digest_type: opts.digestType,
1109
+ digest: opts.digest
1110
+ });
1111
+ tick(`DNSSEC record added to ${domain}`);
1112
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
1113
+ } catch (err) { handleError(err); }
1114
+ });
1115
+
1116
+ dnssecCmd
1117
+ .command('delete <domain>')
1118
+ .description('Delete a DNSSEC DS record')
1119
+ .option('--algorithm <n>', 'Algorithm number')
1120
+ .option('--key-tag <n>', 'Key tag number')
1121
+ .option('--digest-type <n>', 'Digest type number')
1122
+ .option('--digest <hash>', 'Digest hash value')
1123
+ .action(async (domain, opts) => {
1124
+ try {
1125
+ await dns.dnssec.delete(domain, {
1126
+ algorithm: opts.algorithm,
1127
+ key_tag: opts.keyTag,
1128
+ digest_type: opts.digestType,
1129
+ digest: opts.digest
1130
+ });
1131
+ tick(`DNSSEC record deleted from ${domain}`);
1132
+ } catch (err) { handleError(err); }
1133
+ });
1134
+
1135
+ // ─── portfolio ────────────────────────────────────────────────────────────────
1136
+ const portfolioCmd = program.command('portfolio').description('Domain portfolio management');
1137
+
1138
+ portfolioCmd
1139
+ .command('list')
1140
+ .description('List all portfolios')
1141
+ .action(async () => {
1142
+ try {
1143
+ const portfolios = await dns.portfolio.list();
1144
+ output(portfolios, (data) => {
1145
+ if (!data.length) { console.log('No portfolios found.'); return; }
1146
+ const table = new Table({ head: ['Name', 'Domains'], style: { head: ['cyan'] } });
1147
+ data.forEach(p => table.push([p.name || '—', p.domains || 0]));
1148
+ console.log(table.toString());
1149
+ });
1150
+ } catch (err) { handleError(err); }
1151
+ });
1152
+
1153
+ portfolioCmd
1154
+ .command('create <name>')
1155
+ .description('Create a new portfolio')
1156
+ .action(async (name) => {
1157
+ try {
1158
+ const result = await dns.portfolio.create(name);
1159
+ tick(`Portfolio '${name}' created`);
1160
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
1161
+ } catch (err) { handleError(err); }
1162
+ });
1163
+
1164
+ portfolioCmd
1165
+ .command('delete <name>')
1166
+ .description('Delete a portfolio')
1167
+ .action(async (name) => {
1168
+ try {
1169
+ await dns.portfolio.delete(name);
1170
+ tick(`Portfolio '${name}' deleted`);
1171
+ } catch (err) { handleError(err); }
1172
+ });
1173
+
1174
+ portfolioCmd
1175
+ .command('assign <name> <domains...>')
1176
+ .description('Assign domains to a portfolio')
1177
+ .action(async (name, domains) => {
1178
+ try {
1179
+ const result = await dns.portfolio.assign(name, domains);
1180
+ tick(`${domains.length} domain(s) assigned to '${name}'`);
1181
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
1182
+ } catch (err) { handleError(err); }
1183
+ });
1184
+
1185
+ // ─── orders ──────────────────────────────────────────────────────────────────
1186
+ const ordersCmd = program.command('orders').description('Order management');
1187
+
1188
+ ordersCmd
1189
+ .command('list')
1190
+ .description('List all orders')
1191
+ .action(async () => {
1192
+ try {
1193
+ const orderList = await dns.orders.list();
1194
+ output(orderList, (data) => {
1195
+ if (!data.length) { console.log('No orders found.'); return; }
1196
+ const table = new Table({ head: ['Order #', 'Domain', 'Amount', 'Date', 'Status'], style: { head: ['cyan'] } });
1197
+ data.forEach(o => table.push([o.order_number || '—', o.domain || '—', o.amount ? `$${o.amount}` : '—', o.date || '—', o.status || '—']));
1198
+ console.log(table.toString());
1199
+ });
1200
+ } catch (err) { handleError(err); }
1201
+ });
1202
+
1203
+ ordersCmd
1204
+ .command('details <orderNumber>')
1205
+ .description('Get details for a specific order')
1206
+ .action(async (orderNumber) => {
1207
+ try {
1208
+ const order = await dns.orders.details(orderNumber);
1209
+ output(order, (o) => {
1210
+ console.log(`Order: ${o.order_number || orderNumber}`);
1211
+ console.log(` Domain: ${o.domain || '—'}`);
1212
+ console.log(` Amount: $${o.amount || '—'}`);
1213
+ console.log(` Date: ${o.date || '—'}`);
1214
+ console.log(` Status: ${o.status || '—'}`);
1215
+ });
1216
+ } catch (err) { handleError(err); }
1217
+ });
1218
+
1219
+ // ─── push ─────────────────────────────────────────────────────────────────────
1220
+ program
1221
+ .command('push <domain>')
1222
+ .description('Push a domain to another NameSilo account')
1223
+ .option('--to <email>', 'Target account email')
1224
+ .action(async (domain, opts) => {
1225
+ try {
1226
+ if (!opts.to) { console.error('✗ --to <email> is required'); process.exit(1); }
1227
+ const result = await dns.push(domain, opts.to);
1228
+ tick(`Domain ${domain} pushed to ${opts.to}`);
1229
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
1230
+ } catch (err) { handleError(err); }
1231
+ });
1232
+
1233
+ // ─── verify-status ───────────────────────────────────────────────────────────
1234
+ program
1235
+ .command('verify-status')
1236
+ .description('Check registrant verification status')
1237
+ .action(async () => {
1238
+ try {
1239
+ const result = await dns.verificationStatus();
1240
+ output(result, (r) => {
1241
+ console.log(`Verified: ${r.verified ? '✓ Yes' : '✗ No'}`);
1242
+ if (r.email) console.log(`Email: ${r.email}`);
1243
+ });
1244
+ } catch (err) { handleError(err); }
1245
+ });
1246
+
1247
+ // ─── verify-email ─────────────────────────────────────────────────────────────
1248
+ program
1249
+ .command('verify-email <email>')
1250
+ .description('Send email verification for registrant')
1251
+ .action(async (email) => {
1252
+ try {
1253
+ const result = await dns.emailVerify(email);
1254
+ tick(`Verification email sent to ${email}`);
1255
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
1256
+ } catch (err) { handleError(err); }
1257
+ });
1258
+
1259
+ // ─── webhooks ─────────────────────────────────────────────────────────────────
1260
+
1261
+ const webhooksCmd = program.command('webhooks').description('Webhook notification management');
1262
+
1263
+ webhooksCmd
1264
+ .command('list')
1265
+ .description('List registered webhooks')
1266
+ .action(async () => {
1267
+ try {
1268
+ const hooks = await dns.webhooks.list();
1269
+ output(hooks, (data) => {
1270
+ if (!data.length) { console.log('No webhooks registered.'); return; }
1271
+ const table = new Table({ head: ['ID', 'URL', 'Events', 'Active', 'Last Status'], style: { head: ['cyan'] } });
1272
+ data.forEach(h => table.push([h.id.slice(0, 8) + '...', h.url.slice(0, 40), (h.events || []).join(', '), h.active ? '✓' : '✗', h.last_status_code || '—']));
1273
+ console.log(table.toString());
1274
+ });
1275
+ } catch (err) { handleError(err); }
1276
+ });
1277
+
1278
+ webhooksCmd
1279
+ .command('add <url>')
1280
+ .description('Register a webhook URL')
1281
+ .option('--events <events>', 'Comma-separated events (default: domain.expiring,purchase.confirmed)', 'domain.expiring,purchase.confirmed')
1282
+ .option('--secret <secret>', 'Signing secret for HMAC verification')
1283
+ .action(async (url, opts) => {
1284
+ try {
1285
+ const events = opts.events.split(',').map(e => e.trim());
1286
+ const result = await dns.webhooks.add(url, events, opts.secret);
1287
+ tick(`Webhook registered (ID: ${result.id})`);
1288
+ output(result, (r) => {
1289
+ console.log(` URL: ${r.url}`);
1290
+ console.log(` Events: ${(r.events || []).join(', ')}`);
1291
+ console.log(` ID: ${r.id}`);
1292
+ });
1293
+ } catch (err) { handleError(err); }
1294
+ });
1295
+
1296
+ webhooksCmd
1297
+ .command('delete <id>')
1298
+ .description('Delete a webhook by ID')
1299
+ .action(async (id) => {
1300
+ try {
1301
+ await dns.webhooks.delete(id);
1302
+ tick(`Webhook ${id} deleted`);
1303
+ } catch (err) { handleError(err); }
1304
+ });
1305
+
1306
+ webhooksCmd
1307
+ .command('test <id>')
1308
+ .description('Send a test event to a webhook')
1309
+ .action(async (id) => {
1310
+ try {
1311
+ const result = await dns.webhooks.test(id);
1312
+ if (isJson()) console.log(JSON.stringify(result, null, 2));
1313
+ tick(`Test event sent to ${result.url}`);
1314
+ } catch (err) { handleError(err); }
1315
+ });
1316
+
1317
+ // ─── Parse & run ─────────────────────────────────────────────────────────────
1318
+
1319
+ // Verbose timing interceptor: check --verbose directly in argv so it runs before parse
1320
+ const _verboseMode = process.argv.includes('--verbose');
1321
+ if (_verboseMode && typeof fetch !== 'undefined') {
1322
+ const _origFetch = fetch;
1323
+ global.fetch = async function(url, opts) {
1324
+ const _t0 = Date.now();
1325
+ const _res = await _origFetch(url, opts);
1326
+ const _elapsed = Date.now() - _t0;
1327
+ const _urlStr = String(url).replace(/^https?:\/\/[^/]+/, '');
1328
+ console.error(`[verbose] ${(opts && opts.method) || 'GET'} ${_urlStr} → ${_res.status} ${_res.statusText} (${_elapsed}ms)`);
1329
+ return _res;
1330
+ };
1331
+ }
1332
+
1333
+ program.parse(process.argv);
1334
+
1335
+ // Show help if no command given
1336
+ if (!process.argv.slice(2).length) {
1337
+ program.outputHelp();
1338
+ }