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