nodaddy 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +118 -0
  2. package/dist/index.js +1977 -0
  3. package/package.json +56 -0
package/dist/index.js ADDED
@@ -0,0 +1,1977 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/services/state-manager.ts
13
+ var state_manager_exports = {};
14
+ __export(state_manager_exports, {
15
+ clearAll: () => clearAll,
16
+ clearConfig: () => clearConfig,
17
+ createMigration: () => createMigration,
18
+ getActiveMigration: () => getActiveMigration,
19
+ getAllMigrations: () => getAllMigrations,
20
+ getConfig: () => getConfig,
21
+ getMigration: () => getMigration,
22
+ getResumableDomains: () => getResumableDomains,
23
+ getStorePath: () => getStorePath,
24
+ saveDnsBackup: () => saveDnsBackup,
25
+ setConfig: () => setConfig,
26
+ updateDomainStatus: () => updateDomainStatus
27
+ });
28
+ import Conf from "conf";
29
+ import crypto from "crypto";
30
+ function getConfig() {
31
+ return store.get("config");
32
+ }
33
+ function setConfig(config) {
34
+ const current = store.get("config");
35
+ store.set("config", { ...current, ...config });
36
+ }
37
+ function clearConfig() {
38
+ store.set("config", {});
39
+ }
40
+ function clearAll() {
41
+ store.clear();
42
+ }
43
+ function getStorePath() {
44
+ return store.path;
45
+ }
46
+ function createMigration(domains) {
47
+ const id = crypto.randomUUID();
48
+ const now = (/* @__PURE__ */ new Date()).toISOString();
49
+ const domainStates = {};
50
+ for (const domain of domains) {
51
+ domainStates[domain] = {
52
+ domain,
53
+ status: "pending",
54
+ dnsRecordsBackup: [],
55
+ lastUpdated: now
56
+ };
57
+ }
58
+ const migration = {
59
+ id,
60
+ startedAt: now,
61
+ domains: domainStates
62
+ };
63
+ const migrations = store.get("migrations");
64
+ migrations[id] = migration;
65
+ store.set("migrations", migrations);
66
+ store.set("activeMigrationId", id);
67
+ return migration;
68
+ }
69
+ function sanitizeMigration(migration) {
70
+ for (const domain of Object.values(migration.domains)) {
71
+ delete domain.authCode;
72
+ }
73
+ return migration;
74
+ }
75
+ function getActiveMigration() {
76
+ const id = store.get("activeMigrationId");
77
+ if (!id) return null;
78
+ const migrations = store.get("migrations");
79
+ const migration = migrations[id];
80
+ return migration ? sanitizeMigration(migration) : null;
81
+ }
82
+ function getMigration(id) {
83
+ const migrations = store.get("migrations");
84
+ const migration = migrations[id];
85
+ return migration ? sanitizeMigration(migration) : null;
86
+ }
87
+ function updateDomainStatus(migrationId, domain, status, extra) {
88
+ const migrations = store.get("migrations");
89
+ const migration = migrations[migrationId];
90
+ if (!migration) throw new Error(`Migration ${migrationId} not found`);
91
+ const domainState = migration.domains[domain];
92
+ if (!domainState) throw new Error(`Domain ${domain} not in migration`);
93
+ migration.domains[domain] = {
94
+ ...domainState,
95
+ // Clear stale error when transitioning to a non-failed status
96
+ ...status !== "failed" ? { error: void 0 } : {},
97
+ ...extra,
98
+ status,
99
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
100
+ };
101
+ store.set("migrations", migrations);
102
+ }
103
+ function saveDnsBackup(migrationId, domain, records) {
104
+ updateDomainStatus(migrationId, domain, "pending", {
105
+ dnsRecordsBackup: records
106
+ });
107
+ }
108
+ function getAllMigrations() {
109
+ const migrations = store.get("migrations");
110
+ return Object.values(migrations).sort(
111
+ (a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
112
+ );
113
+ }
114
+ function getResumableDomains(migrationId) {
115
+ const migration = getMigration(migrationId);
116
+ if (!migration) return [];
117
+ return Object.values(migration.domains).filter(
118
+ (d) => d.status !== "completed" && d.status !== "transfer_initiated"
119
+ );
120
+ }
121
+ var store;
122
+ var init_state_manager = __esm({
123
+ "src/services/state-manager.ts"() {
124
+ "use strict";
125
+ store = new Conf({
126
+ projectName: "nodaddy",
127
+ configFileMode: 384,
128
+ defaults: {
129
+ config: {},
130
+ migrations: {},
131
+ activeMigrationId: null
132
+ }
133
+ });
134
+ }
135
+ });
136
+
137
+ // src/index.ts
138
+ import { Command } from "commander";
139
+
140
+ // src/commands/migrate.ts
141
+ import * as p4 from "@clack/prompts";
142
+ import chalk5 from "chalk";
143
+
144
+ // src/providers/godaddy.ts
145
+ import { z as z2 } from "zod/v4";
146
+
147
+ // src/types/godaddy.ts
148
+ import { z } from "zod/v4";
149
+ var GoDaddyDomainSchema = z.object({
150
+ domain: z.string(),
151
+ domainId: z.number(),
152
+ status: z.string(),
153
+ expires: z.string().optional(),
154
+ expirationProtected: z.boolean().optional(),
155
+ holdRegistrar: z.boolean().optional(),
156
+ locked: z.boolean().optional(),
157
+ privacy: z.boolean().optional(),
158
+ renewAuto: z.boolean().optional(),
159
+ renewable: z.boolean().optional(),
160
+ transferProtected: z.boolean().optional(),
161
+ createdAt: z.string().optional(),
162
+ authCode: z.string().optional(),
163
+ nameServers: z.array(z.string()).nullable().optional()
164
+ });
165
+ var GoDaddyDnsRecordSchema = z.object({
166
+ type: z.string(),
167
+ name: z.string(),
168
+ data: z.string(),
169
+ ttl: z.number(),
170
+ priority: z.number().optional(),
171
+ weight: z.number().optional(),
172
+ port: z.number().optional(),
173
+ service: z.string().optional(),
174
+ protocol: z.string().optional()
175
+ });
176
+
177
+ // src/services/rate-limiter.ts
178
+ var RateLimiter = class {
179
+ timestamps = [];
180
+ config;
181
+ constructor(config) {
182
+ this.config = config;
183
+ }
184
+ async acquire() {
185
+ const now = Date.now();
186
+ this.timestamps = this.timestamps.filter(
187
+ (t) => now - t < this.config.windowMs
188
+ );
189
+ if (this.timestamps.length >= this.config.requests) {
190
+ const oldest = this.timestamps[0];
191
+ const waitMs = this.config.windowMs - (now - oldest) + 50;
192
+ await sleep(waitMs);
193
+ return this.acquire();
194
+ }
195
+ this.timestamps.push(now);
196
+ }
197
+ };
198
+ function sleep(ms) {
199
+ return new Promise((resolve) => setTimeout(resolve, ms));
200
+ }
201
+ var godaddyRateLimiter = new RateLimiter({
202
+ requests: 55,
203
+ // 60/min with buffer
204
+ windowMs: 6e4
205
+ });
206
+ var cloudflareRateLimiter = new RateLimiter({
207
+ requests: 1100,
208
+ // 1200/5min with buffer
209
+ windowMs: 3e5
210
+ });
211
+
212
+ // src/services/validation.ts
213
+ var DOMAIN_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/;
214
+ function assertValidDomain(domain) {
215
+ if (domain.length === 0 || domain.length > 253) {
216
+ throw new Error(`Invalid domain name: ${domain}`);
217
+ }
218
+ const labels = domain.split(".");
219
+ for (const label of labels) {
220
+ if (label.length === 0 || label.length > 63) {
221
+ throw new Error(`Invalid domain name: ${domain}`);
222
+ }
223
+ }
224
+ if (!DOMAIN_RE.test(domain)) {
225
+ throw new Error(`Invalid domain name: ${domain}`);
226
+ }
227
+ }
228
+
229
+ // src/providers/godaddy.ts
230
+ var BASE_URL = "https://api.godaddy.com";
231
+ var RESOURCE_LOCK_MAX_RETRIES = 6;
232
+ var RESOURCE_LOCK_BASE_DELAY_MS = 5e3;
233
+ var GoDaddyClient = class {
234
+ credentials;
235
+ constructor(credentials) {
236
+ this.credentials = credentials;
237
+ }
238
+ /**
239
+ * Core fetch with 422 resource lock retry. All request methods use this.
240
+ */
241
+ async fetchWithRetry(path, options = {}, onRetry) {
242
+ for (let attempt = 0; attempt <= RESOURCE_LOCK_MAX_RETRIES; attempt++) {
243
+ await godaddyRateLimiter.acquire();
244
+ const res = await fetch(`${BASE_URL}${path}`, {
245
+ ...options,
246
+ headers: {
247
+ Authorization: `sso-key ${this.credentials.apiKey}:${this.credentials.apiSecret}`,
248
+ "Content-Type": "application/json",
249
+ ...options.headers
250
+ }
251
+ });
252
+ if (res.ok) return res;
253
+ const body = await res.text();
254
+ if (res.status === 422 && body.includes("Resource is being used") && attempt < RESOURCE_LOCK_MAX_RETRIES) {
255
+ const delay = RESOURCE_LOCK_BASE_DELAY_MS * (attempt + 1);
256
+ onRetry?.(attempt + 1, RESOURCE_LOCK_MAX_RETRIES, delay / 1e3);
257
+ await new Promise((r) => setTimeout(r, delay));
258
+ continue;
259
+ }
260
+ throw new GoDaddyApiError(
261
+ `GoDaddy API error ${res.status}: ${body}`,
262
+ res.status,
263
+ body
264
+ );
265
+ }
266
+ throw new Error("Exhausted retries");
267
+ }
268
+ async request(path, options = {}, onRetry) {
269
+ const res = await this.fetchWithRetry(path, options, onRetry);
270
+ const text2 = await res.text();
271
+ if (!text2) throw new Error("Expected JSON response but got empty body");
272
+ return JSON.parse(text2);
273
+ }
274
+ async requestVoid(path, options = {}, onRetry) {
275
+ const res = await this.fetchWithRetry(path, options, onRetry);
276
+ await res.text();
277
+ }
278
+ async requestText(path, onRetry) {
279
+ const res = await this.fetchWithRetry(path, {}, onRetry);
280
+ return res.text();
281
+ }
282
+ async listDomains() {
283
+ const data = await this.request(
284
+ "/v1/domains?limit=1000&statuses=ACTIVE"
285
+ );
286
+ return z2.array(GoDaddyDomainSchema).parse(data);
287
+ }
288
+ async getDomainDetail(domain) {
289
+ assertValidDomain(domain);
290
+ const data = await this.request(`/v1/domains/${domain}`);
291
+ return GoDaddyDomainSchema.parse(data);
292
+ }
293
+ async getDnsRecords(domain) {
294
+ assertValidDomain(domain);
295
+ const data = await this.request(
296
+ `/v1/domains/${domain}/records`
297
+ );
298
+ return z2.array(GoDaddyDnsRecordSchema).parse(data);
299
+ }
300
+ async removePrivacy(domain, onRetry) {
301
+ assertValidDomain(domain);
302
+ await this.requestVoid(
303
+ `/v1/domains/${domain}/privacy`,
304
+ { method: "DELETE" },
305
+ onRetry
306
+ );
307
+ }
308
+ async prepareForTransfer(domain, onRetry) {
309
+ assertValidDomain(domain);
310
+ try {
311
+ await this.requestVoid(
312
+ `/v1/domains/${domain}`,
313
+ { method: "PATCH", body: JSON.stringify({ locked: false, renewAuto: false }) },
314
+ onRetry
315
+ );
316
+ } catch (err) {
317
+ if (err instanceof GoDaddyApiError && !err.responseBody.includes("Resource is being used")) {
318
+ await this.requestVoid(
319
+ `/v1/domains/${domain}`,
320
+ { method: "PATCH", body: JSON.stringify({ locked: false }) },
321
+ onRetry
322
+ );
323
+ } else {
324
+ throw err;
325
+ }
326
+ }
327
+ }
328
+ async getAuthCode(domain) {
329
+ assertValidDomain(domain);
330
+ try {
331
+ const text2 = await this.requestText(
332
+ `/v1/domains/${domain}/transferAuthCode`
333
+ );
334
+ try {
335
+ const parsed = JSON.parse(text2);
336
+ if (typeof parsed === "string") return parsed;
337
+ if (Array.isArray(parsed) && typeof parsed[0] === "string")
338
+ return parsed[0];
339
+ } catch {
340
+ }
341
+ return text2.replace(/^"|"$/g, "");
342
+ } catch (err) {
343
+ if (err instanceof GoDaddyApiError && err.statusCode === 404) {
344
+ const detail = await this.getDomainDetail(domain);
345
+ if (detail.authCode) return detail.authCode;
346
+ throw new GoDaddyApiError(
347
+ `No auth code available for ${domain} \u2014 check GoDaddy dashboard`,
348
+ 404,
349
+ ""
350
+ );
351
+ }
352
+ throw err;
353
+ }
354
+ }
355
+ async updateNameservers(domain, nameservers, onRetry) {
356
+ assertValidDomain(domain);
357
+ await this.requestVoid(
358
+ `/v1/domains/${domain}`,
359
+ { method: "PATCH", body: JSON.stringify({ nameServers: nameservers }) },
360
+ onRetry
361
+ );
362
+ }
363
+ async verifyCredentials() {
364
+ try {
365
+ await this.request("/v1/domains?limit=1");
366
+ return true;
367
+ } catch {
368
+ return false;
369
+ }
370
+ }
371
+ };
372
+ var GoDaddyApiError = class extends Error {
373
+ constructor(message, statusCode, responseBody) {
374
+ super(message);
375
+ this.statusCode = statusCode;
376
+ this.responseBody = responseBody;
377
+ this.name = "GoDaddyApiError";
378
+ }
379
+ };
380
+
381
+ // src/providers/cloudflare.ts
382
+ import { z as z4 } from "zod/v4";
383
+
384
+ // src/types/cloudflare.ts
385
+ import { z as z3 } from "zod/v4";
386
+ var CloudflareZoneSchema = z3.object({
387
+ id: z3.string(),
388
+ name: z3.string(),
389
+ status: z3.string(),
390
+ name_servers: z3.array(z3.string()).optional()
391
+ });
392
+ var CloudflareDnsRecordSchema = z3.object({
393
+ id: z3.string().optional(),
394
+ type: z3.string(),
395
+ name: z3.string(),
396
+ content: z3.string(),
397
+ ttl: z3.number(),
398
+ proxied: z3.boolean().optional(),
399
+ priority: z3.number().optional(),
400
+ data: z3.record(z3.string(), z3.unknown()).optional()
401
+ });
402
+
403
+ // src/providers/cloudflare.ts
404
+ var BASE_URL2 = "https://api.cloudflare.com/client/v4";
405
+ var CloudflareClient = class {
406
+ credentials;
407
+ constructor(credentials) {
408
+ this.credentials = credentials;
409
+ }
410
+ authHeaders() {
411
+ if (this.credentials.authType === "global-key") {
412
+ return {
413
+ "X-Auth-Key": this.credentials.apiKey,
414
+ "X-Auth-Email": this.credentials.email
415
+ };
416
+ }
417
+ return {
418
+ Authorization: `Bearer ${this.credentials.apiToken}`
419
+ };
420
+ }
421
+ async request(path, options = {}) {
422
+ await cloudflareRateLimiter.acquire();
423
+ const res = await fetch(`${BASE_URL2}${path}`, {
424
+ ...options,
425
+ headers: {
426
+ ...this.authHeaders(),
427
+ "Content-Type": "application/json",
428
+ ...options.headers
429
+ }
430
+ });
431
+ if (!res.ok) {
432
+ const body = await res.text();
433
+ throw new CloudflareApiError(
434
+ `Cloudflare API error ${res.status}: ${body}`,
435
+ res.status,
436
+ body
437
+ );
438
+ }
439
+ const text2 = await res.text();
440
+ if (!text2) throw new Error("Expected JSON response but got empty body");
441
+ const json = JSON.parse(text2);
442
+ if (!json.success) {
443
+ const errorMsg = json.errors.map((e) => `${e.code}: ${e.message}`).join(", ");
444
+ throw new CloudflareApiError(
445
+ `Cloudflare API error: ${errorMsg}`,
446
+ res.status,
447
+ JSON.stringify(json)
448
+ );
449
+ }
450
+ return json.result;
451
+ }
452
+ async createZone(domain) {
453
+ assertValidDomain(domain);
454
+ const data = await this.request("/zones", {
455
+ method: "POST",
456
+ body: JSON.stringify({
457
+ name: domain,
458
+ account: { id: this.credentials.accountId },
459
+ jump_start: true,
460
+ type: "full"
461
+ })
462
+ });
463
+ return CloudflareZoneSchema.parse(data);
464
+ }
465
+ async getZoneByName(domain) {
466
+ assertValidDomain(domain);
467
+ const data = await this.request(
468
+ `/zones?name=${encodeURIComponent(domain)}&account.id=${this.credentials.accountId}`
469
+ );
470
+ const zones = z4.array(CloudflareZoneSchema).parse(data);
471
+ return zones[0] ?? null;
472
+ }
473
+ async getZoneStatus(zoneId) {
474
+ const data = await this.request(`/zones/${zoneId}`);
475
+ return CloudflareZoneSchema.parse(data);
476
+ }
477
+ async createDnsRecord(zoneId, record) {
478
+ const data = await this.request(
479
+ `/zones/${zoneId}/dns_records`,
480
+ {
481
+ method: "POST",
482
+ body: JSON.stringify(record)
483
+ }
484
+ );
485
+ return CloudflareDnsRecordSchema.parse(data);
486
+ }
487
+ async verifyCredentials() {
488
+ try {
489
+ const endpoint = this.credentials.authType === "global-key" ? "/user" : "/user/tokens/verify";
490
+ await this.request(endpoint);
491
+ return true;
492
+ } catch {
493
+ return false;
494
+ }
495
+ }
496
+ async checkAuthCode(domain, authCode) {
497
+ assertValidDomain(domain);
498
+ const encoded = Buffer.from(authCode).toString("base64");
499
+ return this.request(
500
+ `/accounts/${this.credentials.accountId}/registrar/domains/${domain}/check_auth`,
501
+ {
502
+ method: "POST",
503
+ body: JSON.stringify({ auth_code: encoded })
504
+ }
505
+ );
506
+ }
507
+ async initiateTransfer(zoneId, domain, authCode, contact) {
508
+ assertValidDomain(domain);
509
+ const encoded = Buffer.from(authCode).toString("base64");
510
+ return this.request(
511
+ `/zones/${zoneId}/registrar/domains/${domain}/transfer`,
512
+ {
513
+ method: "POST",
514
+ body: JSON.stringify({
515
+ auth_code: encoded,
516
+ auto_renew: true,
517
+ years: 1,
518
+ privacy: true,
519
+ import_dns: true,
520
+ registrant: contact,
521
+ fee_acknowledgement: {
522
+ transfer_fee: 0,
523
+ icann_fee: 0
524
+ }
525
+ })
526
+ }
527
+ );
528
+ }
529
+ async waitForZoneActive(zoneId, timeoutMs = 3e5, pollIntervalMs = 1e4) {
530
+ const start = Date.now();
531
+ while (Date.now() - start < timeoutMs) {
532
+ const zone = await this.getZoneStatus(zoneId);
533
+ if (zone.status === "active") return zone;
534
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
535
+ }
536
+ throw new CloudflareApiError(
537
+ `Zone ${zoneId} did not become active within ${timeoutMs / 1e3}s`,
538
+ 408,
539
+ ""
540
+ );
541
+ }
542
+ };
543
+ var CloudflareApiError = class extends Error {
544
+ constructor(message, statusCode, responseBody) {
545
+ super(message);
546
+ this.statusCode = statusCode;
547
+ this.responseBody = responseBody;
548
+ this.name = "CloudflareApiError";
549
+ }
550
+ };
551
+
552
+ // src/ui/wizard.ts
553
+ init_state_manager();
554
+ import * as p from "@clack/prompts";
555
+ import chalk from "chalk";
556
+ function credentialsFromEnv() {
557
+ const gdKey = process.env.GODADDY_API_KEY;
558
+ const gdSecret = process.env.GODADDY_API_SECRET;
559
+ const cfAccountId = process.env.CLOUDFLARE_ACCOUNT_ID;
560
+ if (!gdKey || !gdSecret || !cfAccountId) return null;
561
+ const cfApiKey = process.env.CLOUDFLARE_API_KEY;
562
+ const cfEmail = process.env.CLOUDFLARE_EMAIL;
563
+ const cfToken = process.env.CLOUDFLARE_API_TOKEN;
564
+ let cloudflare;
565
+ if (cfApiKey && cfEmail) {
566
+ cloudflare = { authType: "global-key", apiKey: cfApiKey, email: cfEmail, accountId: cfAccountId };
567
+ } else if (cfToken) {
568
+ cloudflare = { authType: "token", apiToken: cfToken, accountId: cfAccountId };
569
+ } else {
570
+ return null;
571
+ }
572
+ return {
573
+ godaddy: { apiKey: gdKey, apiSecret: gdSecret },
574
+ cloudflare
575
+ };
576
+ }
577
+ async function collectCredentials() {
578
+ const envCreds = credentialsFromEnv();
579
+ if (envCreds) {
580
+ p.log.info("Using credentials from environment variables.");
581
+ return envCreds;
582
+ }
583
+ const config = getConfig();
584
+ const hasGoDaddy = config.godaddy?.apiKey && config.godaddy?.apiSecret;
585
+ const hasCloudflare = config.cloudflare?.accountId && (config.cloudflare?.apiToken || config.cloudflare?.apiKey);
586
+ let useStored = false;
587
+ if (hasGoDaddy && hasCloudflare) {
588
+ useStored = await p.confirm({
589
+ message: "Use stored API credentials?",
590
+ initialValue: true
591
+ });
592
+ if (p.isCancel(useStored)) {
593
+ p.cancel("Migration cancelled.");
594
+ process.exit(0);
595
+ }
596
+ }
597
+ if (useStored && hasGoDaddy && hasCloudflare) {
598
+ const cf = config.cloudflare;
599
+ const cloudflare2 = cf.authType === "global-key" ? { authType: "global-key", apiKey: cf.apiKey, email: cf.email, accountId: cf.accountId } : { authType: "token", apiToken: cf.apiToken, accountId: cf.accountId };
600
+ return {
601
+ godaddy: config.godaddy,
602
+ cloudflare: cloudflare2
603
+ };
604
+ }
605
+ p.note(
606
+ "GoDaddy: Create a Production API key (not OTE/Test) at\n https://developer.godaddy.com/keys\n\nCloudflare: Use your Global API Key (bottom of page) at\n https://dash.cloudflare.com/profile/api-tokens\n Account ID is on any zone overview page.",
607
+ "API Credentials Required"
608
+ );
609
+ const gdCreds = await p.group(
610
+ {
611
+ gdKey: () => p.text({
612
+ message: "GoDaddy API Key",
613
+ placeholder: "e.g. dLf3...",
614
+ validate: (v) => {
615
+ if (!v?.trim()) return "API key is required";
616
+ }
617
+ }),
618
+ gdSecret: () => p.password({
619
+ message: "GoDaddy API Secret",
620
+ validate: (v) => {
621
+ if (!v?.trim()) return "API secret is required";
622
+ }
623
+ })
624
+ },
625
+ { onCancel: () => {
626
+ p.cancel("Migration cancelled.");
627
+ process.exit(0);
628
+ } }
629
+ );
630
+ const cfAuthType = await p.select({
631
+ message: "Cloudflare auth method",
632
+ options: [
633
+ { value: "global-key", label: "Global API Key", hint: "recommended \u2014 supports registrar transfers" },
634
+ { value: "token", label: "Scoped API Token", hint: "limited \u2014 no registrar transfer support" }
635
+ ]
636
+ });
637
+ if (p.isCancel(cfAuthType)) {
638
+ p.cancel("Migration cancelled.");
639
+ process.exit(0);
640
+ }
641
+ let cloudflare;
642
+ if (cfAuthType === "global-key") {
643
+ const cfCreds = await p.group(
644
+ {
645
+ email: () => p.text({
646
+ message: "Cloudflare account email",
647
+ placeholder: "you@example.com",
648
+ validate: (v) => {
649
+ if (!v?.trim()) return "Email is required";
650
+ }
651
+ }),
652
+ apiKey: () => p.password({
653
+ message: "Cloudflare Global API Key",
654
+ validate: (v) => {
655
+ if (!v?.trim()) return "API key is required";
656
+ }
657
+ }),
658
+ accountId: () => p.text({
659
+ message: "Cloudflare Account ID",
660
+ placeholder: "Found on any zone overview page",
661
+ validate: (v) => {
662
+ if (!v?.trim()) return "Account ID is required";
663
+ }
664
+ })
665
+ },
666
+ { onCancel: () => {
667
+ p.cancel("Migration cancelled.");
668
+ process.exit(0);
669
+ } }
670
+ );
671
+ cloudflare = {
672
+ authType: "global-key",
673
+ apiKey: cfCreds.apiKey,
674
+ email: cfCreds.email,
675
+ accountId: cfCreds.accountId
676
+ };
677
+ } else {
678
+ const cfCreds = await p.group(
679
+ {
680
+ apiToken: () => p.password({
681
+ message: "Cloudflare API Token",
682
+ validate: (v) => {
683
+ if (!v?.trim()) return "API token is required";
684
+ }
685
+ }),
686
+ accountId: () => p.text({
687
+ message: "Cloudflare Account ID",
688
+ placeholder: "Found on any zone overview page",
689
+ validate: (v) => {
690
+ if (!v?.trim()) return "Account ID is required";
691
+ }
692
+ })
693
+ },
694
+ { onCancel: () => {
695
+ p.cancel("Migration cancelled.");
696
+ process.exit(0);
697
+ } }
698
+ );
699
+ cloudflare = {
700
+ authType: "token",
701
+ apiToken: cfCreds.apiToken,
702
+ accountId: cfCreds.accountId
703
+ };
704
+ }
705
+ const save = await p.confirm({
706
+ message: "Save credentials for future use?",
707
+ initialValue: true
708
+ });
709
+ if (p.isCancel(save)) {
710
+ p.cancel("Migration cancelled.");
711
+ process.exit(0);
712
+ }
713
+ const result = {
714
+ godaddy: {
715
+ apiKey: gdCreds.gdKey,
716
+ apiSecret: gdCreds.gdSecret
717
+ },
718
+ cloudflare
719
+ };
720
+ if (save) {
721
+ setConfig({
722
+ godaddy: result.godaddy,
723
+ cloudflare: cloudflare.authType === "global-key" ? { authType: "global-key", apiKey: cloudflare.apiKey, email: cloudflare.email, accountId: cloudflare.accountId } : { authType: "token", apiToken: cloudflare.apiToken, accountId: cloudflare.accountId }
724
+ });
725
+ p.log.success("Credentials saved.");
726
+ }
727
+ return result;
728
+ }
729
+ async function collectMigrationOptions(overrides) {
730
+ const options = await p.group(
731
+ {
732
+ migrateRecords: () => p.confirm({
733
+ message: "Migrate DNS records to Cloudflare?",
734
+ initialValue: true
735
+ }),
736
+ proxied: () => p.confirm({
737
+ message: "Proxy records through Cloudflare (orange cloud)?",
738
+ initialValue: false
739
+ })
740
+ },
741
+ {
742
+ onCancel: () => {
743
+ p.cancel("Migration cancelled.");
744
+ process.exit(0);
745
+ }
746
+ }
747
+ );
748
+ let dryRun = overrides?.dryRun ?? false;
749
+ if (!overrides?.dryRun) {
750
+ const answer = await p.confirm({
751
+ message: "Dry run first? (preview without making changes)",
752
+ initialValue: false
753
+ });
754
+ if (p.isCancel(answer)) {
755
+ p.cancel("Migration cancelled.");
756
+ process.exit(0);
757
+ }
758
+ dryRun = answer;
759
+ }
760
+ return {
761
+ migrateRecords: options.migrateRecords,
762
+ proxied: options.proxied,
763
+ dryRun
764
+ };
765
+ }
766
+ async function collectRegistrantContact() {
767
+ const config = getConfig();
768
+ if (config.registrantContact) {
769
+ const saved = config.registrantContact;
770
+ const useStored = await p.confirm({
771
+ message: `Use saved registrant contact? (${saved.first_name} ${saved.last_name}, ${saved.email})`,
772
+ initialValue: true
773
+ });
774
+ if (p.isCancel(useStored)) {
775
+ p.cancel("Migration cancelled.");
776
+ process.exit(0);
777
+ }
778
+ if (useStored) return saved;
779
+ }
780
+ p.note(
781
+ "ICANN requires registrant contact info for all domain transfers.\nCloudflare enables free WHOIS privacy by default, so this\ninformation will not be publicly visible after the transfer.",
782
+ "Registrant Contact"
783
+ );
784
+ const contact = await p.group(
785
+ {
786
+ first_name: () => p.text({
787
+ message: "First name",
788
+ validate: (v) => {
789
+ if (!v?.trim()) return "Required";
790
+ }
791
+ }),
792
+ last_name: () => p.text({
793
+ message: "Last name",
794
+ validate: (v) => {
795
+ if (!v?.trim()) return "Required";
796
+ }
797
+ }),
798
+ email: () => p.text({
799
+ message: "Email",
800
+ placeholder: "you@example.com",
801
+ validate: (v) => {
802
+ if (!v?.trim()) return "Required";
803
+ }
804
+ }),
805
+ phone: () => p.text({
806
+ message: "Phone",
807
+ placeholder: "+1.5551234567",
808
+ validate: (v) => {
809
+ if (!v?.trim()) return "Required";
810
+ }
811
+ }),
812
+ address: () => p.text({
813
+ message: "Street address",
814
+ validate: (v) => {
815
+ if (!v?.trim()) return "Required";
816
+ }
817
+ }),
818
+ address2: () => p.text({
819
+ message: "Address line 2 (optional)",
820
+ defaultValue: ""
821
+ }),
822
+ city: () => p.text({
823
+ message: "City",
824
+ validate: (v) => {
825
+ if (!v?.trim()) return "Required";
826
+ }
827
+ }),
828
+ state: () => p.text({
829
+ message: "State / Province",
830
+ validate: (v) => {
831
+ if (!v?.trim()) return "Required";
832
+ }
833
+ }),
834
+ zip: () => p.text({
835
+ message: "Postal code",
836
+ validate: (v) => {
837
+ if (!v?.trim()) return "Required";
838
+ }
839
+ }),
840
+ country: () => p.text({
841
+ message: "Country code",
842
+ placeholder: "US",
843
+ validate: (v) => {
844
+ if (!v?.trim()) return "Required";
845
+ }
846
+ })
847
+ },
848
+ { onCancel: () => {
849
+ p.cancel("Migration cancelled.");
850
+ process.exit(0);
851
+ } }
852
+ );
853
+ const result = {
854
+ first_name: contact.first_name,
855
+ last_name: contact.last_name,
856
+ organization: "",
857
+ address: contact.address,
858
+ address2: contact.address2 ?? "",
859
+ city: contact.city,
860
+ state: contact.state,
861
+ zip: contact.zip,
862
+ country: contact.country,
863
+ phone: contact.phone,
864
+ email: contact.email
865
+ };
866
+ setConfig({ registrantContact: result });
867
+ p.log.success("Registrant contact saved for future transfers.");
868
+ return result;
869
+ }
870
+ async function confirmTransferCost(domainCount) {
871
+ p.note(
872
+ `Each domain transfer includes a 1-year renewal charged at
873
+ Cloudflare's at-cost pricing. Cost varies by TLD \u2014 common
874
+ examples: .com ~$9.15, .net ~$10.50, .org ~$10.00/year.
875
+ Other TLDs may cost more. Check Cloudflare's pricing for details.
876
+
877
+ Payment is billed to the card on file in your Cloudflare account.
878
+ Domains to transfer: ${chalk.bold(domainCount)}`,
879
+ "Transfer Cost"
880
+ );
881
+ const confirmed = await p.confirm({
882
+ message: `I understand that ${chalk.bold(domainCount)} domain transfer${domainCount === 1 ? "" : "s"} will be charged to my Cloudflare account`,
883
+ initialValue: true
884
+ });
885
+ if (p.isCancel(confirmed)) {
886
+ p.cancel("Migration cancelled.");
887
+ process.exit(0);
888
+ }
889
+ return confirmed;
890
+ }
891
+ async function confirmMigration(domainCount, dryRun) {
892
+ const action = dryRun ? "preview migration for" : "start migration for";
893
+ const confirmed = await p.confirm({
894
+ message: `Proceed to ${action} ${chalk.bold(domainCount)} domain${domainCount === 1 ? "" : "s"}?`,
895
+ initialValue: true
896
+ });
897
+ if (p.isCancel(confirmed)) {
898
+ p.cancel("Migration cancelled.");
899
+ process.exit(0);
900
+ }
901
+ return confirmed;
902
+ }
903
+
904
+ // src/ui/domain-selector.ts
905
+ import * as p2 from "@clack/prompts";
906
+ import chalk2 from "chalk";
907
+ async function selectDomains(domains, selectAll) {
908
+ if (selectAll) {
909
+ return domains.map((d) => d.domain);
910
+ }
911
+ const mode = await p2.select({
912
+ message: `${domains.length} domains available \u2014 migrate all or choose?`,
913
+ options: [
914
+ { value: "all", label: "All domains" },
915
+ { value: "pick", label: "Let me choose" }
916
+ ]
917
+ });
918
+ if (p2.isCancel(mode)) {
919
+ p2.cancel("Migration cancelled.");
920
+ process.exit(0);
921
+ }
922
+ if (mode === "all") {
923
+ return domains.map((d) => d.domain);
924
+ }
925
+ const options = domains.map((d) => {
926
+ const expires = d.expires ? new Date(d.expires).toLocaleDateString() : "unknown";
927
+ const locked = d.locked ? "locked" : "unlocked";
928
+ const privacy = d.privacy ? "privacy" : "no privacy";
929
+ let expiryWarning = "";
930
+ if (d.expires) {
931
+ const daysUntilExpiry = (new Date(d.expires).getTime() - Date.now()) / (1e3 * 60 * 60 * 24);
932
+ if (daysUntilExpiry < 30) {
933
+ expiryWarning = chalk2.yellow(" \u26A0 expires soon");
934
+ }
935
+ }
936
+ return {
937
+ value: d.domain,
938
+ label: d.domain,
939
+ hint: `expires ${expires}, ${locked}, ${privacy}${expiryWarning}`
940
+ };
941
+ });
942
+ const selected = await p2.multiselect({
943
+ message: `Select domains to migrate (${domains.length} available)`,
944
+ options,
945
+ required: true
946
+ });
947
+ if (p2.isCancel(selected)) {
948
+ p2.cancel("Migration cancelled.");
949
+ process.exit(0);
950
+ }
951
+ return selected;
952
+ }
953
+
954
+ // src/ui/dns-preview.ts
955
+ import * as p3 from "@clack/prompts";
956
+ import chalk3 from "chalk";
957
+
958
+ // src/services/dns-migrator.ts
959
+ var SKIP_TYPES = /* @__PURE__ */ new Set(["SOA"]);
960
+ function isGoDaddyParking(record) {
961
+ if (record.type === "A" && record.name === "@" && record.data === "Parked") {
962
+ return true;
963
+ }
964
+ if (record.type === "CNAME" && (record.data.endsWith(".secureserver.net") || record.data.endsWith(".domaincontrol.com"))) {
965
+ return true;
966
+ }
967
+ return false;
968
+ }
969
+ function mapGoDaddyToCloudflare(records, domain, proxied = false) {
970
+ const mapped = [];
971
+ for (const record of records) {
972
+ if (SKIP_TYPES.has(record.type)) continue;
973
+ if (record.type === "NS" && record.name === "@") continue;
974
+ if (isGoDaddyParking(record)) continue;
975
+ const cfRecord = mapRecord(record, domain, proxied);
976
+ if (cfRecord) mapped.push(cfRecord);
977
+ }
978
+ return mapped;
979
+ }
980
+ function mapRecord(record, domain, proxied) {
981
+ const name = record.name === "@" ? domain : `${record.name}.${domain}`;
982
+ const ttl = record.ttl < 120 ? 1 : record.ttl;
983
+ switch (record.type) {
984
+ case "A":
985
+ case "AAAA":
986
+ return {
987
+ type: record.type,
988
+ name,
989
+ content: record.data,
990
+ ttl,
991
+ proxied
992
+ };
993
+ case "CNAME":
994
+ return {
995
+ type: "CNAME",
996
+ name,
997
+ content: record.data === "@" ? domain : record.data,
998
+ ttl,
999
+ proxied
1000
+ };
1001
+ case "MX":
1002
+ return {
1003
+ type: "MX",
1004
+ name,
1005
+ content: record.data,
1006
+ ttl,
1007
+ priority: record.priority ?? 10
1008
+ };
1009
+ case "TXT":
1010
+ return {
1011
+ type: "TXT",
1012
+ name,
1013
+ content: record.data,
1014
+ ttl
1015
+ };
1016
+ case "SRV": {
1017
+ const srvName = record.service && record.protocol ? `${record.service}.${record.protocol}.${name}` : name;
1018
+ return {
1019
+ type: "SRV",
1020
+ name: srvName,
1021
+ content: `${record.priority ?? 0} ${record.weight ?? 0} ${record.port ?? 0} ${record.data}`,
1022
+ ttl,
1023
+ data: {
1024
+ priority: record.priority ?? 0,
1025
+ weight: record.weight ?? 0,
1026
+ port: record.port ?? 0,
1027
+ target: record.data,
1028
+ service: record.service ?? "",
1029
+ proto: record.protocol ?? "",
1030
+ name
1031
+ }
1032
+ };
1033
+ }
1034
+ case "CAA": {
1035
+ const parts = record.data.match(/^(\d+)\s+(\S+)\s+(.+)$/);
1036
+ if (!parts) return null;
1037
+ return {
1038
+ type: "CAA",
1039
+ name,
1040
+ content: record.data,
1041
+ ttl,
1042
+ data: {
1043
+ flags: parseInt(parts[1], 10),
1044
+ tag: parts[2],
1045
+ value: parts[3]
1046
+ }
1047
+ };
1048
+ }
1049
+ case "NS":
1050
+ return {
1051
+ type: "NS",
1052
+ name,
1053
+ content: record.data,
1054
+ ttl
1055
+ };
1056
+ default:
1057
+ return null;
1058
+ }
1059
+ }
1060
+ async function migrateDnsRecords(cloudflare, zoneId, records) {
1061
+ let created = 0;
1062
+ const failed = [];
1063
+ for (const record of records) {
1064
+ try {
1065
+ await cloudflare.createDnsRecord(zoneId, record);
1066
+ created++;
1067
+ } catch (err) {
1068
+ const message = err instanceof Error ? err.message : String(err);
1069
+ if (message.includes("already exists")) {
1070
+ created++;
1071
+ } else {
1072
+ failed.push({ record, error: message });
1073
+ }
1074
+ }
1075
+ }
1076
+ return { created, failed };
1077
+ }
1078
+
1079
+ // src/ui/dns-preview.ts
1080
+ function summarizeRecords(records) {
1081
+ const counts = /* @__PURE__ */ new Map();
1082
+ for (const r of records) {
1083
+ counts.set(r.type, (counts.get(r.type) ?? 0) + 1);
1084
+ }
1085
+ const parts = [...counts.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([type, count]) => `${count} ${type}`);
1086
+ return `${parts.join(", ")} ${chalk3.dim(`(${records.length} total)`)}`;
1087
+ }
1088
+ function formatRecordDetail(record) {
1089
+ const type = chalk3.cyan(record.type.padEnd(6));
1090
+ const name = chalk3.bold(record.name);
1091
+ const content = record.content.length > 60 ? record.content.slice(0, 57) + "..." : record.content;
1092
+ const priority = record.priority !== void 0 ? ` (pri: ${record.priority})` : "";
1093
+ return ` ${type} ${name} \u2192 ${content}${priority}`;
1094
+ }
1095
+ async function previewDnsRecords(godaddy, domains) {
1096
+ const s = p3.spinner();
1097
+ s.start(`Fetching DNS records for ${domains.length} domain${domains.length === 1 ? "" : "s"}...`);
1098
+ const previews = [];
1099
+ const errors = [];
1100
+ for (const domain of domains) {
1101
+ try {
1102
+ const gdRecords = await godaddy.getDnsRecords(domain);
1103
+ const cfRecords = mapGoDaddyToCloudflare(gdRecords, domain);
1104
+ previews.push({ domain, records: cfRecords });
1105
+ } catch (err) {
1106
+ errors.push({ domain, error: err instanceof Error ? err.message : String(err) });
1107
+ }
1108
+ }
1109
+ s.stop("DNS records fetched");
1110
+ p3.log.message(chalk3.bold("DNS records to migrate:"));
1111
+ for (const { domain, records } of previews) {
1112
+ if (records.length === 0) {
1113
+ p3.log.message(` ${chalk3.bold(domain)} \u2192 ${chalk3.dim("no records to migrate")}`);
1114
+ } else {
1115
+ p3.log.message(` ${chalk3.bold(domain)} \u2192 ${summarizeRecords(records)}`);
1116
+ }
1117
+ }
1118
+ for (const { domain, error } of errors) {
1119
+ p3.log.message(` ${chalk3.bold(domain)} \u2192 ${chalk3.red(`failed: ${error}`)}`);
1120
+ }
1121
+ const domainsWithRecords = previews.filter((p9) => p9.records.length > 0);
1122
+ if (domainsWithRecords.length === 0) return;
1123
+ let viewMore = true;
1124
+ while (viewMore) {
1125
+ const choice = await p3.select({
1126
+ message: "View detailed records for a domain?",
1127
+ options: [
1128
+ { value: "__skip__", label: "Continue", hint: "proceed to migration options" },
1129
+ ...domainsWithRecords.map((d) => ({
1130
+ value: d.domain,
1131
+ label: d.domain,
1132
+ hint: `${d.records.length} records`
1133
+ }))
1134
+ ]
1135
+ });
1136
+ if (p3.isCancel(choice) || choice === "__skip__") {
1137
+ viewMore = false;
1138
+ break;
1139
+ }
1140
+ const preview = domainsWithRecords.find((d) => d.domain === choice);
1141
+ if (preview) {
1142
+ p3.log.message(chalk3.bold(`
1143
+ ${preview.domain} records:`));
1144
+ for (const record of preview.records) {
1145
+ p3.log.message(formatRecordDetail(record));
1146
+ }
1147
+ p3.log.message("");
1148
+ }
1149
+ }
1150
+ }
1151
+
1152
+ // src/services/transfer-engine.ts
1153
+ init_state_manager();
1154
+
1155
+ // src/services/errors.ts
1156
+ import chalk4 from "chalk";
1157
+ var GODADDY_HINTS = [
1158
+ {
1159
+ match: (_, status) => status === 401 || status === 403,
1160
+ suggestion: "Check your GoDaddy API key and secret at https://developer.godaddy.com/keys"
1161
+ },
1162
+ {
1163
+ match: (_, status) => status === 429,
1164
+ suggestion: "GoDaddy rate limit hit. Wait a minute and try again, or reduce concurrency."
1165
+ },
1166
+ {
1167
+ match: (msg) => msg.includes("DOMAIN_LOCKED"),
1168
+ suggestion: "Domain is locked. It may take a few minutes after unlocking before GoDaddy reflects the change."
1169
+ },
1170
+ {
1171
+ match: (msg) => msg.includes("409") || msg.includes("Conflict"),
1172
+ suggestion: "The auth code may have been sent to your email instead. Check your inbox and use `nodaddy resume` to continue."
1173
+ },
1174
+ {
1175
+ match: (msg) => msg.includes("UNABLE_TO_AUTHENTICATE") || msg.includes("NOT_FOUND"),
1176
+ suggestion: "Make sure you are using a Production API key (not OTE/Test) at https://developer.godaddy.com/keys"
1177
+ }
1178
+ ];
1179
+ var CLOUDFLARE_HINTS = [
1180
+ {
1181
+ match: (_, status) => status === 401 || status === 403,
1182
+ suggestion: "Check your Cloudflare API token permissions. Required: Zone:Edit, DNS:Edit, Registrar Domains:Edit"
1183
+ },
1184
+ {
1185
+ match: (msg) => msg.includes("already exists"),
1186
+ suggestion: "This zone already exists in Cloudflare. You may need to delete it first or use the existing zone."
1187
+ },
1188
+ {
1189
+ match: (_, status) => status === 429,
1190
+ suggestion: "Cloudflare rate limit hit. The tool will automatically retry with backoff."
1191
+ },
1192
+ {
1193
+ match: (msg) => msg.includes("not_registrable"),
1194
+ suggestion: "This TLD cannot be transferred to Cloudflare Registrar. DNS-only setup is still possible."
1195
+ },
1196
+ {
1197
+ match: (msg) => msg.includes("did not become active"),
1198
+ suggestion: "Nameserver changes can take up to 48 hours to propagate. Run `nodaddy resume` to retry later."
1199
+ }
1200
+ ];
1201
+ function formatError(err, provider, plain = false) {
1202
+ const message = err instanceof Error ? err.message : String(err);
1203
+ const statusCode = err.statusCode;
1204
+ const hints = provider === "godaddy" ? GODADDY_HINTS : provider === "cloudflare" ? CLOUDFLARE_HINTS : [...GODADDY_HINTS, ...CLOUDFLARE_HINTS];
1205
+ const hint = hints.find((h) => h.match(message, statusCode));
1206
+ if (hint) {
1207
+ return plain ? `${message} | Suggestion: ${hint.suggestion}` : `${message}
1208
+ ${chalk4.yellow("Suggestion:")} ${hint.suggestion}`;
1209
+ }
1210
+ return message;
1211
+ }
1212
+
1213
+ // src/services/transfer-engine.ts
1214
+ var GODADDY_RESOURCE_LOCK_DELAY_MS = 5e3;
1215
+ var UNSUPPORTED_TLDS = /* @__PURE__ */ new Set([
1216
+ "uk",
1217
+ "co.uk",
1218
+ "org.uk",
1219
+ "me.uk",
1220
+ "de",
1221
+ "ca",
1222
+ "au",
1223
+ "com.au",
1224
+ "net.au",
1225
+ "jp",
1226
+ "eu",
1227
+ "be",
1228
+ "fr",
1229
+ "nl"
1230
+ ]);
1231
+ function preflightCheck(domain) {
1232
+ const reasons = [];
1233
+ if (domain.status !== "ACTIVE") {
1234
+ reasons.push(
1235
+ `Status is ${domain.status} \u2014 domain must be ACTIVE to transfer. Check your GoDaddy dashboard for holds or suspensions.`
1236
+ );
1237
+ }
1238
+ if (domain.createdAt) {
1239
+ const created = new Date(domain.createdAt);
1240
+ const daysSinceCreation = (Date.now() - created.getTime()) / (1e3 * 60 * 60 * 24);
1241
+ if (daysSinceCreation < 60) {
1242
+ const daysRemaining = Math.ceil(60 - daysSinceCreation);
1243
+ reasons.push(
1244
+ `Domain is only ${Math.floor(daysSinceCreation)} days old \u2014 ICANN requires 60 days before transfer. Try again in ${daysRemaining} day${daysRemaining === 1 ? "" : "s"}.`
1245
+ );
1246
+ }
1247
+ }
1248
+ const tld = domain.domain.split(".").slice(1).join(".");
1249
+ if (UNSUPPORTED_TLDS.has(tld)) {
1250
+ reasons.push(
1251
+ `TLD .${tld} is not supported by Cloudflare Registrar \u2014 see https://www.cloudflare.com/tld-policies/ for supported TLDs`
1252
+ );
1253
+ }
1254
+ if (domain.transferProtected) {
1255
+ reasons.push(
1256
+ "Domain Protection is enabled \u2014 disable at https://dcc.godaddy.com \u2192 select domain \u2192 Secure \u2192 downgrade to None (requires identity verification)"
1257
+ );
1258
+ }
1259
+ return {
1260
+ domain: domain.domain,
1261
+ eligible: reasons.length === 0,
1262
+ reasons
1263
+ };
1264
+ }
1265
+ async function transferDomain(godaddy, cloudflare, domain, migrationId, options, contact, onProgress) {
1266
+ const report = (step, status, error) => {
1267
+ onProgress?.({ domain, step, status, error });
1268
+ };
1269
+ const retryReporter = (action, currentStatus) => (attempt, maxRetries, delaySec) => {
1270
+ report(
1271
+ `${action} \u2014 GoDaddy resource locked, retrying in ${delaySec}s (${attempt}/${maxRetries})`,
1272
+ currentStatus
1273
+ );
1274
+ };
1275
+ try {
1276
+ report("Exporting DNS records", "pending");
1277
+ const dnsRecords = await godaddy.getDnsRecords(domain);
1278
+ saveDnsBackup(migrationId, domain, dnsRecords);
1279
+ if (options.dryRun) {
1280
+ report("Dry run \u2014 would migrate DNS records", "pending");
1281
+ return;
1282
+ }
1283
+ report("Creating Cloudflare zone", "pending");
1284
+ let zone;
1285
+ try {
1286
+ zone = await cloudflare.createZone(domain);
1287
+ } catch (err) {
1288
+ const message = err instanceof Error ? err.message : "";
1289
+ if (message.includes("already exists")) {
1290
+ report("Zone already exists, looking up", "pending");
1291
+ const existing = await cloudflare.getZoneByName(domain);
1292
+ if (existing) {
1293
+ zone = existing;
1294
+ } else {
1295
+ throw err;
1296
+ }
1297
+ } else {
1298
+ throw err;
1299
+ }
1300
+ }
1301
+ const zoneId = zone.id;
1302
+ const nameservers = zone.name_servers ?? [];
1303
+ updateDomainStatus(migrationId, domain, "pending", {
1304
+ cloudflareZoneId: zoneId,
1305
+ cloudflareNameservers: nameservers
1306
+ });
1307
+ if (options.migrateRecords) {
1308
+ report("Migrating DNS records", "pending");
1309
+ const cfRecords = mapGoDaddyToCloudflare(dnsRecords, domain, options.proxied);
1310
+ const result = await migrateDnsRecords(cloudflare, zoneId, cfRecords);
1311
+ if (result.failed.length > 0) {
1312
+ report(
1313
+ `DNS migration: ${result.created} created, ${result.failed.length} failed`,
1314
+ "pending"
1315
+ );
1316
+ }
1317
+ }
1318
+ updateDomainStatus(migrationId, domain, "dns_migrated");
1319
+ report("DNS migrated", "dns_migrated");
1320
+ report("Removing WHOIS privacy", "dns_migrated");
1321
+ try {
1322
+ await godaddy.removePrivacy(domain, retryReporter("Removing WHOIS privacy", "dns_migrated"));
1323
+ } catch (privacyErr) {
1324
+ const msg = privacyErr instanceof Error ? privacyErr.message : "";
1325
+ if (!msg.includes("404") && !msg.includes("409")) {
1326
+ report(`Privacy removal failed (non-blocking): ${msg}`, "dns_migrated");
1327
+ }
1328
+ }
1329
+ await new Promise((r) => setTimeout(r, GODADDY_RESOURCE_LOCK_DELAY_MS));
1330
+ report("Unlocking + disabling auto-renew", "dns_migrated");
1331
+ await godaddy.prepareForTransfer(domain, retryReporter("Unlocking + disabling auto-renew", "dns_migrated"));
1332
+ report("Waiting for unlock to propagate", "dns_migrated");
1333
+ let unlocked = false;
1334
+ for (let attempt = 0; attempt < 6; attempt++) {
1335
+ await new Promise((r) => setTimeout(r, GODADDY_RESOURCE_LOCK_DELAY_MS));
1336
+ const detail = await godaddy.getDomainDetail(domain);
1337
+ if (!detail.locked) {
1338
+ unlocked = true;
1339
+ break;
1340
+ }
1341
+ }
1342
+ if (!unlocked) {
1343
+ throw new Error(
1344
+ `Domain ${domain} is still locked after prepareForTransfer (unlock + disable auto-renew)`
1345
+ );
1346
+ }
1347
+ updateDomainStatus(migrationId, domain, "unlocked");
1348
+ report("Domain unlocked", "unlocked");
1349
+ report("Fetching auth code", "unlocked");
1350
+ const authCode = await godaddy.getAuthCode(domain);
1351
+ updateDomainStatus(migrationId, domain, "auth_obtained");
1352
+ report("Auth code obtained", "auth_obtained");
1353
+ if (nameservers.length > 0) {
1354
+ report("Updating nameservers", "auth_obtained");
1355
+ await godaddy.updateNameservers(domain, nameservers, retryReporter("Updating nameservers", "auth_obtained"));
1356
+ updateDomainStatus(migrationId, domain, "ns_changed");
1357
+ report("Nameservers updated", "ns_changed");
1358
+ }
1359
+ if (contact) {
1360
+ report("Waiting for zone activation (may take a few minutes)", "ns_changed");
1361
+ await cloudflare.waitForZoneActive(zoneId);
1362
+ report("Validating auth code", "ns_changed");
1363
+ await cloudflare.checkAuthCode(domain, authCode);
1364
+ report("Initiating transfer", "ns_changed");
1365
+ await cloudflare.initiateTransfer(zoneId, domain, authCode, contact);
1366
+ updateDomainStatus(migrationId, domain, "transfer_initiated");
1367
+ report("Transfer initiated", "transfer_initiated");
1368
+ } else {
1369
+ updateDomainStatus(migrationId, domain, "completed");
1370
+ report("DNS migrated (no registrar transfer \u2014 use Global API Key to enable)", "completed");
1371
+ }
1372
+ return { authCode };
1373
+ } catch (err) {
1374
+ const rawMessage = err instanceof Error ? err.message : String(err);
1375
+ const provider = rawMessage.includes("GoDaddy") ? "godaddy" : rawMessage.includes("Cloudflare") ? "cloudflare" : void 0;
1376
+ const message = formatError(err, provider);
1377
+ const persistedError = formatError(err, provider, true);
1378
+ updateDomainStatus(migrationId, domain, "failed", {
1379
+ error: persistedError
1380
+ });
1381
+ report(message, "failed", message);
1382
+ throw err;
1383
+ }
1384
+ }
1385
+
1386
+ // src/ui/progress.ts
1387
+ import { Listr } from "listr2";
1388
+ function createMigrationTasks(domains, godaddy, cloudflare, migrationId, options, contact) {
1389
+ return new Listr(
1390
+ domains.map((domain) => ({
1391
+ title: domain,
1392
+ task: async (ctx, task) => {
1393
+ try {
1394
+ const result = await transferDomain(
1395
+ godaddy,
1396
+ cloudflare,
1397
+ domain,
1398
+ migrationId,
1399
+ options,
1400
+ contact,
1401
+ (progress) => {
1402
+ task.title = `${domain} \u2014 ${progress.step}`;
1403
+ }
1404
+ );
1405
+ ctx.results.set(domain, {
1406
+ success: true,
1407
+ authCode: result?.authCode
1408
+ });
1409
+ task.title = `${domain} \u2713`;
1410
+ } catch (err) {
1411
+ const message = err instanceof Error ? err.message : String(err);
1412
+ ctx.results.set(domain, { success: false, error: message });
1413
+ task.title = `${domain} \u2717 ${message}`;
1414
+ throw err;
1415
+ }
1416
+ },
1417
+ exitOnError: false
1418
+ })),
1419
+ {
1420
+ concurrent: 8,
1421
+ exitOnError: false,
1422
+ rendererOptions: {
1423
+ collapseErrors: false
1424
+ }
1425
+ }
1426
+ );
1427
+ }
1428
+
1429
+ // src/commands/migrate.ts
1430
+ init_state_manager();
1431
+ async function migrateCommand(opts) {
1432
+ p4.intro(chalk5.bgCyan.black(" nodaddy "));
1433
+ const creds = await collectCredentials();
1434
+ const s = p4.spinner();
1435
+ s.start("Verifying API credentials...");
1436
+ const godaddy = new GoDaddyClient(creds.godaddy);
1437
+ const cloudflare = new CloudflareClient(creds.cloudflare);
1438
+ const [gdValid, cfValid] = await Promise.all([
1439
+ godaddy.verifyCredentials(),
1440
+ cloudflare.verifyCredentials()
1441
+ ]);
1442
+ if (!gdValid) {
1443
+ s.stop("GoDaddy credentials invalid");
1444
+ p4.log.error("Failed to authenticate with GoDaddy API. Check your API key and secret.");
1445
+ process.exit(1);
1446
+ }
1447
+ if (!cfValid) {
1448
+ s.stop("Cloudflare credentials invalid");
1449
+ p4.log.error("Failed to authenticate with Cloudflare API. Check your API token.");
1450
+ process.exit(1);
1451
+ }
1452
+ s.stop("API credentials verified");
1453
+ s.start("Fetching domains from GoDaddy...");
1454
+ let domains;
1455
+ try {
1456
+ domains = await godaddy.listDomains();
1457
+ } catch (err) {
1458
+ s.stop("Failed to fetch domains");
1459
+ p4.log.error(err instanceof Error ? err.message : String(err));
1460
+ process.exit(1);
1461
+ }
1462
+ s.stop(`Found ${chalk5.bold(domains.length)} active domains`);
1463
+ if (domains.length === 0) {
1464
+ p4.log.warn("No active domains found in your GoDaddy account.");
1465
+ p4.outro("Nothing to migrate.");
1466
+ return;
1467
+ }
1468
+ const selected = await selectDomains(domains, opts.all ?? false);
1469
+ if (selected.length === 0) {
1470
+ p4.outro("No domains selected.");
1471
+ return;
1472
+ }
1473
+ s.start("Running preflight checks...");
1474
+ const selectedDomainDetails = await Promise.all(
1475
+ selected.map((d) => godaddy.getDomainDetail(d))
1476
+ );
1477
+ const preflightResults = selectedDomainDetails.map(preflightCheck);
1478
+ const eligible = preflightResults.filter((r) => r.eligible);
1479
+ const ineligible = preflightResults.filter((r) => !r.eligible);
1480
+ s.stop(
1481
+ `${chalk5.green(eligible.length)} eligible, ${chalk5.red(ineligible.length)} ineligible`
1482
+ );
1483
+ if (ineligible.length > 0) {
1484
+ p4.log.warn("Ineligible domains:");
1485
+ for (const result of ineligible) {
1486
+ p4.log.message(
1487
+ ` ${chalk5.red("\u2717")} ${result.domain}: ${result.reasons.join(", ")}`
1488
+ );
1489
+ }
1490
+ }
1491
+ if (eligible.length === 0) {
1492
+ p4.outro("No eligible domains to migrate.");
1493
+ return;
1494
+ }
1495
+ const eligibleDomains = eligible.map((r) => r.domain);
1496
+ await previewDnsRecords(godaddy, eligibleDomains);
1497
+ const migrationOptions = await collectMigrationOptions(
1498
+ opts.dryRun ? { dryRun: true } : void 0
1499
+ );
1500
+ const canTransfer = creds.cloudflare.authType === "global-key";
1501
+ if (!migrationOptions.dryRun && canTransfer) {
1502
+ const costConfirmed = await confirmTransferCost(eligible.length);
1503
+ if (!costConfirmed) {
1504
+ p4.outro("Migration cancelled.");
1505
+ return;
1506
+ }
1507
+ }
1508
+ let contact;
1509
+ if (migrationOptions.dryRun) {
1510
+ contact = void 0;
1511
+ } else if (canTransfer) {
1512
+ contact = await collectRegistrantContact();
1513
+ } else {
1514
+ p4.log.warn(
1515
+ "Scoped API tokens do not support registrar transfers. DNS will be migrated but domains will not be transferred."
1516
+ );
1517
+ contact = void 0;
1518
+ }
1519
+ const confirmed = await confirmMigration(
1520
+ eligible.length,
1521
+ migrationOptions.dryRun
1522
+ );
1523
+ if (!confirmed) {
1524
+ p4.outro("Migration cancelled.");
1525
+ return;
1526
+ }
1527
+ const migration = createMigration(eligibleDomains);
1528
+ p4.log.step(
1529
+ migrationOptions.dryRun ? "Starting dry run..." : "Starting migration..."
1530
+ );
1531
+ const tasks = createMigrationTasks(
1532
+ eligibleDomains,
1533
+ godaddy,
1534
+ cloudflare,
1535
+ migration.id,
1536
+ migrationOptions,
1537
+ contact
1538
+ );
1539
+ const ctx = { results: /* @__PURE__ */ new Map() };
1540
+ try {
1541
+ await tasks.run(ctx);
1542
+ } catch {
1543
+ }
1544
+ const succeeded = eligibleDomains.filter(
1545
+ (d) => ctx.results.get(d)?.success
1546
+ );
1547
+ const failed = eligibleDomains.filter(
1548
+ (d) => !ctx.results.get(d)?.success
1549
+ );
1550
+ if (migrationOptions.dryRun) {
1551
+ p4.log.success(`Preview run finished for ${succeeded.length}/${eligible.length} domains`);
1552
+ } else if (succeeded.length === eligible.length) {
1553
+ p4.log.success(`Migration run finished for ${succeeded.length}/${eligible.length} domains`);
1554
+ } else if (succeeded.length > 0) {
1555
+ p4.log.warn(
1556
+ `Migration run finished: ${chalk5.green(succeeded.length)} succeeded, ${chalk5.red(failed.length)} failed`
1557
+ );
1558
+ } else {
1559
+ p4.log.error(
1560
+ `Migration failed for all ${eligible.length} domain${eligible.length === 1 ? "" : "s"}`
1561
+ );
1562
+ }
1563
+ if (!migrationOptions.dryRun && succeeded.length > 0) {
1564
+ p4.note(
1565
+ `Transfers initiated for ${succeeded.length} domain${succeeded.length === 1 ? "" : "s"}.
1566
+
1567
+ Track progress:
1568
+ ${chalk5.cyan("nodaddy status")}
1569
+
1570
+ https://dash.cloudflare.com/?to=/:account/domains/transfer
1571
+
1572
+ Transfers typically take 1-5 days to complete.
1573
+
1574
+ When you're done transferring domains, run
1575
+ ${chalk5.cyan("nodaddy cleanup")}
1576
+ to remove stored credentials and personal info from this machine.`,
1577
+ "Next Steps"
1578
+ );
1579
+ }
1580
+ if (!migrationOptions.dryRun && failed.length > 0) {
1581
+ p4.note(
1582
+ `${failed.length} domain${failed.length === 1 ? "" : "s"} failed. Your progress has been saved.
1583
+
1584
+ To retry failed domains:
1585
+ ${chalk5.cyan("nodaddy resume")}
1586
+
1587
+ To see what went wrong:
1588
+ ${chalk5.cyan("nodaddy status")}
1589
+
1590
+ Common fixes:
1591
+ \u2022 "Resource is being used" \u2014 GoDaddy is still processing a
1592
+ recent change. Wait a few minutes and run resume.
1593
+ \u2022 Domain Protection \u2014 disable at https://dcc.godaddy.com
1594
+ \u2022 Auth code issues \u2014 check your GoDaddy email inbox`,
1595
+ "Failed Domains"
1596
+ );
1597
+ }
1598
+ p4.outro(chalk5.green("Done!"));
1599
+ }
1600
+
1601
+ // src/commands/list.ts
1602
+ import * as p5 from "@clack/prompts";
1603
+ import chalk6 from "chalk";
1604
+ import Table from "cli-table3";
1605
+ init_state_manager();
1606
+ async function listCommand() {
1607
+ p5.intro(chalk6.bgCyan.black(" nodaddy \u2014 list domains "));
1608
+ const gdKey = process.env.GODADDY_API_KEY;
1609
+ const gdSecret = process.env.GODADDY_API_SECRET;
1610
+ const config = getConfig();
1611
+ const apiKey = gdKey || config.godaddy?.apiKey;
1612
+ const apiSecret = gdSecret || config.godaddy?.apiSecret;
1613
+ if (!apiKey || !apiSecret) {
1614
+ p5.log.error(
1615
+ "GoDaddy credentials not configured. Set GODADDY_API_KEY/GODADDY_API_SECRET env vars, or run `nodaddy migrate`."
1616
+ );
1617
+ process.exit(1);
1618
+ }
1619
+ const godaddy = new GoDaddyClient({ apiKey, apiSecret });
1620
+ const s = p5.spinner();
1621
+ s.start("Fetching domains from GoDaddy...");
1622
+ try {
1623
+ const domains = await godaddy.listDomains();
1624
+ s.stop(`Found ${chalk6.bold(domains.length)} active domains`);
1625
+ if (domains.length === 0) {
1626
+ p5.log.info("No active domains found.");
1627
+ p5.outro("");
1628
+ return;
1629
+ }
1630
+ const table = new Table({
1631
+ head: ["Domain", "Expires", "Locked", "Privacy", "Auto-Renew"],
1632
+ style: { head: ["cyan"] }
1633
+ });
1634
+ for (const d of domains) {
1635
+ const expires = d.expires ? new Date(d.expires).toLocaleDateString() : "\u2014";
1636
+ const locked = d.locked ? chalk6.yellow("Yes") : chalk6.green("No");
1637
+ const privacy = d.privacy ? chalk6.yellow("Yes") : chalk6.dim("No");
1638
+ const autoRenew = d.renewAuto ? chalk6.yellow("Yes") : chalk6.dim("No");
1639
+ table.push([d.domain, expires, locked, privacy, autoRenew]);
1640
+ }
1641
+ console.log(table.toString());
1642
+ p5.outro(`${domains.length} domains total`);
1643
+ } catch (err) {
1644
+ s.stop("Failed to fetch domains");
1645
+ p5.log.error(err instanceof Error ? err.message : String(err));
1646
+ process.exit(1);
1647
+ }
1648
+ }
1649
+
1650
+ // src/commands/status.ts
1651
+ init_state_manager();
1652
+ import * as p6 from "@clack/prompts";
1653
+ import chalk7 from "chalk";
1654
+ import Table2 from "cli-table3";
1655
+ var STATUS_COLORS = {
1656
+ pending: chalk7.dim,
1657
+ dns_migrated: chalk7.blue,
1658
+ unlocked: chalk7.blue,
1659
+ auth_obtained: chalk7.blue,
1660
+ ns_changed: chalk7.cyan,
1661
+ transfer_initiated: chalk7.yellow,
1662
+ completed: chalk7.green,
1663
+ failed: chalk7.red
1664
+ };
1665
+ var STATUS_LABELS = {
1666
+ pending: "Pending",
1667
+ dns_migrated: "DNS Migrated",
1668
+ unlocked: "Unlocked",
1669
+ auth_obtained: "Auth Obtained",
1670
+ ns_changed: "NS Changed",
1671
+ transfer_initiated: "Transferring (1-5 days)",
1672
+ completed: "Completed",
1673
+ failed: "Failed"
1674
+ };
1675
+ var MEANINGFUL_STATUSES = /* @__PURE__ */ new Set([
1676
+ "transfer_initiated",
1677
+ "completed"
1678
+ ]);
1679
+ async function statusCommand() {
1680
+ p6.intro(chalk7.bgCyan.black(" nodaddy \u2014 migration status "));
1681
+ const allMigrations = getAllMigrations();
1682
+ if (allMigrations.length === 0) {
1683
+ p6.log.info("No migrations found. Run `nodaddy migrate` to start one.");
1684
+ p6.outro("");
1685
+ return;
1686
+ }
1687
+ const meaningful = allMigrations.filter(
1688
+ (m) => Object.values(m.domains).some((d) => MEANINGFUL_STATUSES.has(d.status))
1689
+ );
1690
+ if (meaningful.length === 0) {
1691
+ p6.log.info("No completed transfers yet. Run `nodaddy migrate` to start one.");
1692
+ p6.outro("");
1693
+ return;
1694
+ }
1695
+ const domainMap = /* @__PURE__ */ new Map();
1696
+ for (const migration of [...meaningful].reverse()) {
1697
+ for (const d of Object.values(migration.domains)) {
1698
+ if (MEANINGFUL_STATUSES.has(d.status)) {
1699
+ domainMap.set(d.domain, d);
1700
+ }
1701
+ }
1702
+ }
1703
+ const domains = [...domainMap.values()].sort(
1704
+ (a, b) => new Date(a.lastUpdated).getTime() - new Date(b.lastUpdated).getTime()
1705
+ );
1706
+ const table = new Table2({
1707
+ head: ["Domain", "Status", "Last Updated"],
1708
+ style: { head: ["cyan"] }
1709
+ });
1710
+ for (const d of domains) {
1711
+ const colorFn = STATUS_COLORS[d.status] ?? chalk7.dim;
1712
+ const label = STATUS_LABELS[d.status] ?? d.status;
1713
+ const updated = new Date(d.lastUpdated).toLocaleString();
1714
+ const statusText = d.error ? `${colorFn(label)} \u2014 ${chalk7.red(d.error.slice(0, 60))}` : colorFn(label);
1715
+ table.push([d.domain, statusText, updated]);
1716
+ }
1717
+ console.log(table.toString());
1718
+ const counts = domains.reduce(
1719
+ (acc, d) => {
1720
+ acc[d.status] = (acc[d.status] ?? 0) + 1;
1721
+ return acc;
1722
+ },
1723
+ {}
1724
+ );
1725
+ const summary = Object.entries(counts).map(([status, count]) => {
1726
+ const colorFn = STATUS_COLORS[status] ?? chalk7.dim;
1727
+ return colorFn(`${STATUS_LABELS[status] ?? status}: ${count}`);
1728
+ }).join(" | ");
1729
+ p6.outro(`${domains.length} domain${domains.length === 1 ? "" : "s"} \u2014 ${summary}`);
1730
+ }
1731
+
1732
+ // src/commands/resume.ts
1733
+ import * as p7 from "@clack/prompts";
1734
+ import chalk8 from "chalk";
1735
+ init_state_manager();
1736
+ async function resumeCommand() {
1737
+ p7.intro(chalk8.bgCyan.black(" nodaddy \u2014 resume migration "));
1738
+ const migration = getActiveMigration();
1739
+ if (!migration) {
1740
+ const allMigrations = getAllMigrations();
1741
+ if (allMigrations.length === 0) {
1742
+ p7.log.info("No migrations found. Run `nodaddy migrate` to start one.");
1743
+ p7.outro("");
1744
+ return;
1745
+ }
1746
+ p7.log.warn("No active migration. Use `nodaddy status` to review past migrations.");
1747
+ p7.outro("");
1748
+ return;
1749
+ }
1750
+ const resumable = getResumableDomains(migration.id);
1751
+ const allDomains = Object.values(migration.domains);
1752
+ const completed = allDomains.filter(
1753
+ (d) => d.status === "completed" || d.status === "transfer_initiated"
1754
+ );
1755
+ p7.log.info(
1756
+ `Migration ${chalk8.dim(migration.id.slice(0, 8))}: ${completed.length}/${allDomains.length} done, ${resumable.length} remaining`
1757
+ );
1758
+ if (resumable.length === 0) {
1759
+ p7.log.success("All domains have been processed!");
1760
+ p7.outro("");
1761
+ return;
1762
+ }
1763
+ for (const d of resumable) {
1764
+ const statusLabel = d.status === "failed" ? chalk8.red(`failed: ${d.error?.slice(0, 60)}`) : chalk8.yellow(d.status);
1765
+ p7.log.message(` ${d.domain} \u2014 ${statusLabel}`);
1766
+ }
1767
+ const confirmed = await p7.confirm({
1768
+ message: `Resume migration for ${chalk8.bold(resumable.length)} domain${resumable.length === 1 ? "" : "s"}?`,
1769
+ initialValue: true
1770
+ });
1771
+ if (p7.isCancel(confirmed) || !confirmed) {
1772
+ p7.outro("Cancelled.");
1773
+ return;
1774
+ }
1775
+ const config = getConfig();
1776
+ const gd = config.godaddy;
1777
+ const cf = config.cloudflare;
1778
+ if (!gd?.apiKey || !gd?.apiSecret) {
1779
+ p7.log.error("GoDaddy credentials not found. Run `nodaddy migrate` to set them up.");
1780
+ process.exit(1);
1781
+ }
1782
+ if (!cf?.accountId) {
1783
+ p7.log.error("Cloudflare credentials not found. Run `nodaddy migrate` to set them up.");
1784
+ process.exit(1);
1785
+ }
1786
+ if (cf.authType === "global-key" && (!cf.apiKey || !cf.email)) {
1787
+ p7.log.error("Cloudflare Global API Key credentials incomplete. Run `nodaddy migrate` to reconfigure.");
1788
+ process.exit(1);
1789
+ }
1790
+ if (cf.authType === "token" && !cf.apiToken) {
1791
+ p7.log.error("Cloudflare API Token not found. Run `nodaddy migrate` to reconfigure.");
1792
+ process.exit(1);
1793
+ }
1794
+ const cfCreds = cf.authType === "global-key" ? { authType: "global-key", apiKey: cf.apiKey, email: cf.email, accountId: cf.accountId } : { authType: "token", apiToken: cf.apiToken, accountId: cf.accountId };
1795
+ const godaddy = new GoDaddyClient(gd);
1796
+ const cloudflare = new CloudflareClient(cfCreds);
1797
+ let contact;
1798
+ if (cf.authType === "global-key") {
1799
+ contact = await collectRegistrantContact();
1800
+ } else {
1801
+ p7.log.warn(
1802
+ "Scoped API tokens do not support registrar transfers. DNS will be migrated but domains will not be transferred."
1803
+ );
1804
+ contact = void 0;
1805
+ }
1806
+ const domainNames = resumable.map((d) => d.domain);
1807
+ const tasks = createMigrationTasks(
1808
+ domainNames,
1809
+ godaddy,
1810
+ cloudflare,
1811
+ migration.id,
1812
+ { dryRun: false, migrateRecords: true, proxied: false },
1813
+ contact
1814
+ );
1815
+ const ctx = { results: /* @__PURE__ */ new Map() };
1816
+ try {
1817
+ await tasks.run(ctx);
1818
+ } catch {
1819
+ }
1820
+ const succeeded = domainNames.filter((d) => ctx.results.get(d)?.success);
1821
+ const failed = domainNames.filter((d) => !ctx.results.get(d)?.success);
1822
+ if (failed.length === 0) {
1823
+ p7.log.success("All domains resumed successfully.");
1824
+ } else if (succeeded.length > 0) {
1825
+ p7.log.warn(
1826
+ `${chalk8.green(succeeded.length)} succeeded, ${chalk8.red(failed.length)} still failing`
1827
+ );
1828
+ } else {
1829
+ p7.log.error(`All ${domainNames.length} domain${domainNames.length === 1 ? "" : "s"} failed again`);
1830
+ }
1831
+ if (failed.length > 0) {
1832
+ p7.note(
1833
+ `${failed.length} domain${failed.length === 1 ? "" : "s"} still failing. Progress is saved.
1834
+
1835
+ You can run ${chalk8.cyan("nodaddy resume")} again after fixing the issue.
1836
+
1837
+ Common fixes:
1838
+ \u2022 "Resource is being used" \u2014 wait a few minutes and retry
1839
+ \u2022 Domain Protection \u2014 disable at https://dcc.godaddy.com
1840
+ \u2022 Auth code issues \u2014 check your GoDaddy email inbox`,
1841
+ "Still Failing"
1842
+ );
1843
+ } else {
1844
+ p7.note(
1845
+ `Track transfer progress:
1846
+ ${chalk8.cyan("nodaddy status")}
1847
+
1848
+ https://dash.cloudflare.com/?to=/:account/domains/transfer`,
1849
+ "Next Steps"
1850
+ );
1851
+ }
1852
+ p7.outro(chalk8.green("Done!"));
1853
+ }
1854
+
1855
+ // src/commands/cleanup.ts
1856
+ init_state_manager();
1857
+ import * as p8 from "@clack/prompts";
1858
+ import chalk9 from "chalk";
1859
+ async function cleanupCommand() {
1860
+ p8.intro(chalk9.bgCyan.black(" nodaddy \u2014 cleanup "));
1861
+ const config = getConfig();
1862
+ const migrations = getAllMigrations();
1863
+ const storePath = getStorePath();
1864
+ const items = [];
1865
+ if (config.godaddy?.apiKey) {
1866
+ items.push("GoDaddy API credentials");
1867
+ }
1868
+ if (config.cloudflare?.accountId) {
1869
+ items.push("Cloudflare API credentials");
1870
+ }
1871
+ if (config.registrantContact) {
1872
+ const c = config.registrantContact;
1873
+ items.push(`Registrant contact (${c.first_name} ${c.last_name}, ${c.email})`);
1874
+ }
1875
+ if (migrations.length > 0) {
1876
+ const domainCount = migrations.reduce(
1877
+ (sum, m) => sum + Object.keys(m.domains).length,
1878
+ 0
1879
+ );
1880
+ items.push(`${migrations.length} migration${migrations.length === 1 ? "" : "s"} (${domainCount} domain${domainCount === 1 ? "" : "s"})`);
1881
+ }
1882
+ if (items.length === 0) {
1883
+ p8.log.info("Nothing stored. Already clean!");
1884
+ p8.outro("");
1885
+ return;
1886
+ }
1887
+ p8.log.info(`Config file: ${chalk9.dim(storePath)}`);
1888
+ p8.log.message("");
1889
+ p8.log.warn("This will permanently delete:");
1890
+ for (const item of items) {
1891
+ p8.log.message(` ${chalk9.red("\u2022")} ${item}`);
1892
+ }
1893
+ const confirmed = await p8.confirm({
1894
+ message: "Delete all stored data?",
1895
+ initialValue: false
1896
+ });
1897
+ if (p8.isCancel(confirmed) || !confirmed) {
1898
+ p8.outro("Nothing deleted.");
1899
+ return;
1900
+ }
1901
+ clearAll();
1902
+ p8.log.success("All stored data has been deleted.");
1903
+ p8.outro(chalk9.green("Clean!"));
1904
+ }
1905
+
1906
+ // src/index.ts
1907
+ init_state_manager();
1908
+
1909
+ // src/services/signal-handler.ts
1910
+ init_state_manager();
1911
+ function setupSignalHandlers() {
1912
+ const handler = (signal) => {
1913
+ const migration = getActiveMigration();
1914
+ if (migration) {
1915
+ const domains = Object.values(migration.domains);
1916
+ const completed = domains.filter(
1917
+ (d) => d.status === "completed" || d.status === "transfer_initiated"
1918
+ ).length;
1919
+ console.log(
1920
+ `
1921
+
1922
+ Interrupted (${signal}). Migration state saved (${completed}/${domains.length} domains processed).`
1923
+ );
1924
+ console.log("Run `nodaddy resume` to continue.\n");
1925
+ } else {
1926
+ console.log(`
1927
+
1928
+ Interrupted (${signal}).
1929
+ `);
1930
+ }
1931
+ process.exit(1);
1932
+ };
1933
+ process.on("SIGINT", () => handler("SIGINT"));
1934
+ process.on("SIGTERM", () => handler("SIGTERM"));
1935
+ }
1936
+
1937
+ // src/index.ts
1938
+ var program = new Command();
1939
+ program.name("nodaddy").description(
1940
+ "Bulk domain transfer from GoDaddy to Cloudflare"
1941
+ ).version("1.0.0");
1942
+ program.command("migrate").description("Interactive migration wizard").option("--all", "Migrate all domains (skip selection)").option("--dry-run", "Preview without making changes").action(async (opts) => {
1943
+ await migrateCommand(opts);
1944
+ });
1945
+ program.command("list").description("List GoDaddy domains").action(async () => {
1946
+ await listCommand();
1947
+ });
1948
+ program.command("status").description("Check transfer status").action(async () => {
1949
+ await statusCommand();
1950
+ });
1951
+ program.command("resume").description("Resume an interrupted migration").action(async () => {
1952
+ await resumeCommand();
1953
+ });
1954
+ program.command("cleanup").description("Delete all stored credentials, contact info, and migration history").action(async () => {
1955
+ await cleanupCommand();
1956
+ });
1957
+ program.command("config").description("Manage API credentials").option("--reset", "Clear stored credentials").action(async (opts) => {
1958
+ if (opts.reset) {
1959
+ clearConfig();
1960
+ console.log("Credentials cleared.");
1961
+ } else {
1962
+ const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_state_manager(), state_manager_exports));
1963
+ const config = getConfig2();
1964
+ const hasGD = config.godaddy?.apiKey ? "configured" : "not set";
1965
+ const hasCF = config.cloudflare?.accountId ? `configured (${config.cloudflare.authType ?? "token"})` : "not set";
1966
+ console.log(`GoDaddy: ${hasGD}`);
1967
+ console.log(`Cloudflare: ${hasCF}`);
1968
+ console.log(
1969
+ "\nRun `nodaddy migrate` to set up credentials, or `nodaddy config --reset` to clear them."
1970
+ );
1971
+ }
1972
+ });
1973
+ setupSignalHandlers();
1974
+ program.action(async () => {
1975
+ await migrateCommand({});
1976
+ });
1977
+ program.parse();