@tamyla/clodo-framework 2.0.20 → 3.0.2

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 (54) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/bin/clodo-service.js +1 -1
  3. package/bin/database/README.md +33 -0
  4. package/bin/database/deployment-db-manager.js +527 -0
  5. package/bin/database/enterprise-db-manager.js +736 -0
  6. package/bin/database/wrangler-d1-manager.js +775 -0
  7. package/bin/shared/cloudflare/domain-discovery.js +636 -0
  8. package/bin/shared/cloudflare/domain-manager.js +952 -0
  9. package/bin/shared/cloudflare/index.js +8 -0
  10. package/bin/shared/cloudflare/ops.js +359 -0
  11. package/bin/shared/config/index.js +1 -1
  12. package/bin/shared/database/connection-manager.js +374 -0
  13. package/bin/shared/database/index.js +7 -0
  14. package/bin/shared/database/orchestrator.js +726 -0
  15. package/bin/shared/deployment/auditor.js +969 -0
  16. package/bin/shared/deployment/index.js +10 -0
  17. package/bin/shared/deployment/rollback-manager.js +570 -0
  18. package/bin/shared/deployment/validator.js +779 -0
  19. package/bin/shared/index.js +32 -0
  20. package/bin/shared/monitoring/health-checker.js +484 -0
  21. package/bin/shared/monitoring/index.js +8 -0
  22. package/bin/shared/monitoring/memory-manager.js +387 -0
  23. package/bin/shared/monitoring/production-monitor.js +391 -0
  24. package/bin/shared/production-tester/api-tester.js +82 -0
  25. package/bin/shared/production-tester/auth-tester.js +132 -0
  26. package/bin/shared/production-tester/core.js +197 -0
  27. package/bin/shared/production-tester/database-tester.js +109 -0
  28. package/bin/shared/production-tester/index.js +77 -0
  29. package/bin/shared/production-tester/load-tester.js +131 -0
  30. package/bin/shared/production-tester/performance-tester.js +103 -0
  31. package/bin/shared/security/api-token-manager.js +312 -0
  32. package/bin/shared/security/index.js +8 -0
  33. package/bin/shared/security/secret-generator.js +937 -0
  34. package/bin/shared/security/secure-token-manager.js +398 -0
  35. package/bin/shared/utils/error-recovery.js +225 -0
  36. package/bin/shared/utils/graceful-shutdown-manager.js +390 -0
  37. package/bin/shared/utils/index.js +9 -0
  38. package/bin/shared/utils/interactive-prompts.js +146 -0
  39. package/bin/shared/utils/interactive-utils.js +530 -0
  40. package/bin/shared/utils/rate-limiter.js +246 -0
  41. package/dist/database/database-orchestrator.js +34 -12
  42. package/dist/deployment/index.js +2 -2
  43. package/dist/orchestration/multi-domain-orchestrator.js +8 -6
  44. package/dist/service-management/GenerationEngine.js +76 -28
  45. package/dist/service-management/ServiceInitializer.js +5 -3
  46. package/dist/shared/cloudflare/domain-manager.js +1 -1
  47. package/dist/shared/cloudflare/ops.js +27 -12
  48. package/dist/shared/config/index.js +1 -1
  49. package/dist/shared/deployment/index.js +2 -2
  50. package/dist/shared/security/secret-generator.js +4 -2
  51. package/dist/shared/utils/error-recovery.js +1 -1
  52. package/dist/shared/utils/graceful-shutdown-manager.js +4 -3
  53. package/dist/utils/deployment/secret-generator.js +19 -6
  54. package/package.json +4 -2
@@ -0,0 +1,952 @@
1
+ /**
2
+ * Cloudflare Domain & Service Manager
3
+ * Comprehensive domain verification and service status management
4
+ *
5
+ * Handles:
6
+ * 1. Cloudflare authentication verification
7
+ * 2. Domain availability checking
8
+ * 3. Existing service discovery and matching
9
+ * 4. Service status verification
10
+ * 5. Deployment permission management
11
+ */
12
+
13
+ import { execSync } from 'child_process';
14
+ import { promisify } from 'util';
15
+ import { exec } from 'child_process';
16
+ import { askChoice, askYesNo } from '../utils/interactive-prompts.js';
17
+ import { DomainDiscovery } from './domain-discovery.js';
18
+ import { MultiDomainOrchestrator } from '../../../dist/orchestration/multi-domain-orchestrator.js';
19
+ import { getCommandConfig } from '../config/command-config-manager.js';
20
+ import { CloudflareTokenManager } from '../security/api-token-manager.js';
21
+
22
+ const execAsync = promisify(exec);
23
+
24
+ export class CloudflareDomainManager {
25
+ constructor(options = {}) {
26
+ this.apiToken = options.apiToken;
27
+ this.accountId = options.accountId;
28
+ this.isAuthenticated = false;
29
+ this.availableDomains = [];
30
+ this.deployedServices = [];
31
+
32
+ // Initialize command configuration
33
+ this.cmdConfig = getCommandConfig();
34
+
35
+ // Initialize API token manager
36
+ this.tokenManager = new CloudflareTokenManager();
37
+
38
+ // Initialize existing modules
39
+ this.domainDiscovery = new DomainDiscovery({
40
+ apiToken: this.apiToken,
41
+ enableCaching: true
42
+ });
43
+
44
+ this.orchestrator = new MultiDomainOrchestrator({
45
+ maxConcurrentDeployments: 1,
46
+ timeout: 300000
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Step 1: Verify Cloudflare authentication
52
+ */
53
+ async verifyAuthentication() {
54
+ console.log('🔐 Verifying Cloudflare authentication...');
55
+
56
+ try {
57
+ const whoamiCmd = this.cmdConfig.getCloudflareCommand('whoami');
58
+ const { stdout } = await execAsync(whoamiCmd);
59
+
60
+ if (stdout.includes('You are not authenticated') || stdout.includes('not logged in')) {
61
+ return await this.handleAuthenticationRequired();
62
+ }
63
+
64
+ // Extract and display account information
65
+ const accountInfo = this.parseAccountInfo(stdout);
66
+ console.log(' ✅ Cloudflare: authenticated');
67
+ if (accountInfo.email) {
68
+ console.log(` 📧 Account: ${accountInfo.email}`);
69
+ }
70
+ if (accountInfo.accountId) {
71
+ console.log(` 🆔 Account ID: ${accountInfo.accountId}`);
72
+ this.accountId = accountInfo.accountId;
73
+ }
74
+ if (accountInfo.accountName) {
75
+ console.log(` 🏢 Account Name: ${accountInfo.accountName}`);
76
+ }
77
+
78
+ this.isAuthenticated = true;
79
+ return true;
80
+
81
+ } catch (error) {
82
+ return await this.handleAuthenticationRequired();
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Handle authentication requirement
88
+ */
89
+ async handleAuthenticationRequired() {
90
+ console.log(' ❌ Cloudflare authentication required');
91
+
92
+ const authChoice = await askChoice(
93
+ 'Cloudflare authentication needed. What would you like to do?',
94
+ [
95
+ 'Login to Cloudflare now',
96
+ 'Provide API token manually',
97
+ 'Skip Cloudflare verification (limited features)',
98
+ 'Cancel deployment'
99
+ ],
100
+ 0
101
+ );
102
+
103
+ switch (authChoice) {
104
+ case 0:
105
+ return await this.performCloudflareLogin();
106
+ case 1:
107
+ return await this.setApiToken();
108
+ case 2:
109
+ console.log(' ⚠️ Skipping Cloudflare verification - some features unavailable');
110
+ return false;
111
+ case 3:
112
+ throw new Error('Deployment cancelled - authentication required');
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Perform Cloudflare login
118
+ */
119
+ async performCloudflareLogin() {
120
+ try {
121
+ console.log('🔑 Opening Cloudflare authentication...');
122
+ const authCmd = this.cmdConfig.getCloudflareCommand('auth_login');
123
+ await execAsync(authCmd, { stdio: 'inherit' });
124
+
125
+ // Verify login worked
126
+ const whoamiCmd = this.cmdConfig.getCloudflareCommand('whoami');
127
+ const { stdout } = await execAsync(whoamiCmd);
128
+ if (!stdout.includes('You are not authenticated')) {
129
+ console.log(' ✅ Cloudflare authentication successful');
130
+ this.isAuthenticated = true;
131
+ return true;
132
+ } else {
133
+ throw new Error('Authentication verification failed');
134
+ }
135
+ } catch (error) {
136
+ console.log(` ❌ Authentication failed: ${error.message}`);
137
+ return false;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Set API token manually
143
+ */
144
+ async setApiToken() {
145
+ const { askUser } = await import('../utils/interactive-prompts.js');
146
+
147
+ const token = await askUser('Enter your Cloudflare API Token:');
148
+ if (!token || token.trim() === '') {
149
+ console.log(' ❌ API token is required');
150
+ return false;
151
+ }
152
+
153
+ // Test the token
154
+ try {
155
+ const whoamiCmd = this.cmdConfig.getCloudflareCommand('whoami');
156
+ const { stdout } = await execAsync(`CLOUDFLARE_API_TOKEN=${token} ${whoamiCmd}`);
157
+ console.log(' ✅ API token verified successfully');
158
+ this.apiToken = token;
159
+ this.isAuthenticated = true;
160
+ return true;
161
+ } catch (error) {
162
+ console.log(' ❌ Invalid API token');
163
+ return false;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Step 2: Get available domains from Cloudflare using existing domain discovery
169
+ */
170
+ async getAvailableDomains() {
171
+ if (!this.isAuthenticated) {
172
+ console.log(' ⚠️ Skipping domain discovery - not authenticated');
173
+ return [];
174
+ }
175
+
176
+ console.log('🌐 Discovering available domains from Cloudflare...');
177
+
178
+ try {
179
+ // Use existing domain discovery module
180
+ await this.domainDiscovery.initializeDiscovery();
181
+
182
+ // Get domains from wrangler deployments (updated command)
183
+ let services = [];
184
+ try {
185
+ const deploymentsCmd = this.cmdConfig.getCloudflareCommand('deployments_list');
186
+ const { stdout } = await execAsync(deploymentsCmd);
187
+ services = this.parseWranglerDeployments(stdout);
188
+ } catch (error) {
189
+ // Fallback: try to get workers list
190
+ try {
191
+ const listWorkersCmd = this.cmdConfig.getCloudflareCommand('list_workers');
192
+ const { stdout: workersOutput } = await execAsync(listWorkersCmd);
193
+ // If wrangler dev works, try alternate commands
194
+ console.log(' 📋 Using alternative service discovery...');
195
+ } catch {
196
+ console.log(' 📋 No existing deployments found');
197
+ }
198
+ }
199
+
200
+ // Extract unique domains from services
201
+ const serviceDomains = [...new Set(services.map(s => s.domain).filter(Boolean))];
202
+
203
+ // Try to get additional domains from orchestrator
204
+ let orchestratorDomains = [];
205
+ try {
206
+ const portfolioInfo = await this.orchestrator.getPortfolioStatus();
207
+ orchestratorDomains = portfolioInfo.domains || [];
208
+ } catch (error) {
209
+ // Orchestrator domains not available, continue with service domains
210
+ }
211
+
212
+ // Combine and deduplicate domains
213
+ const allDomains = [...new Set([...serviceDomains, ...orchestratorDomains])];
214
+ this.availableDomains = allDomains;
215
+
216
+ console.log(` 📋 Found ${allDomains.length} available domains`);
217
+ allDomains.forEach(domain => console.log(` - ${domain}`));
218
+
219
+ return allDomains;
220
+ } catch (error) {
221
+ console.log(` ⚠️ Could not retrieve domains: ${error.message}`);
222
+ return [];
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Step 3: Verify domain availability and match existing services
228
+ */
229
+ async verifyDomainAndMatchServices(requestedDomain) {
230
+ console.log(`🔍 Verifying domain: ${requestedDomain}`);
231
+
232
+ // Check if domain exists in Cloudflare (with root domain analysis)
233
+ const domainCheck = await this.checkDomainInCloudflare(requestedDomain);
234
+
235
+ if (!domainCheck.found) {
236
+ return await this.handleNewDomain(requestedDomain, domainCheck);
237
+ }
238
+
239
+ // Domain/root domain exists - check for existing services
240
+ if (domainCheck.isSubdomain) {
241
+ console.log(' ✅ Root domain exists - service subdomain can be deployed');
242
+ } else {
243
+ console.log(' ✅ Domain found in Cloudflare');
244
+ }
245
+
246
+ return await this.checkExistingServices(requestedDomain, domainCheck);
247
+ }
248
+
249
+ /**
250
+ * Handle completely new domain or missing root domain
251
+ */
252
+ async handleNewDomain(domain, domainCheck) {
253
+ if (domainCheck.isSubdomain) {
254
+ console.log(' ❌ Root domain not found in Cloudflare account');
255
+ console.log(` ⚠️ Cannot deploy ${domain} because ${domainCheck.rootDomain} is not managed in this account`);
256
+
257
+ // Show what domains ARE available in this account
258
+ await this.showAvailableDomainsInAccount();
259
+
260
+ // Generate domain suggestions
261
+ await this.showDomainSuggestions(domain, domainCheck);
262
+ } else {
263
+ console.log(' 🆕 This is a new root domain - ready for first deployment');
264
+ console.log(' 💡 This is normal for newly created domains');
265
+ }
266
+
267
+ // Show account context for troubleshooting
268
+ console.log(' 📋 Account Context:');
269
+ const accountDetails = await this.getAccountDetails();
270
+
271
+ if (accountDetails.error) {
272
+ console.log(` ⚠️ Could not fetch account details: ${accountDetails.error}`);
273
+ } else {
274
+ if (accountDetails.accountId) {
275
+ console.log(` 🆔 Account ID: ${accountDetails.accountId}`);
276
+ }
277
+ if (accountDetails.totalZones !== undefined) {
278
+ console.log(` 🌐 Total zones in account: ${accountDetails.totalZones}`);
279
+ if (accountDetails.zones && accountDetails.zones.length > 0) {
280
+ console.log(` 📄 Sample zones: ${accountDetails.zones.slice(0, 3).join(', ')}${accountDetails.totalZones > 3 ? '...' : ''}`);
281
+ }
282
+ }
283
+ }
284
+
285
+ // Different choices based on domain type
286
+ let choicePrompt, choiceOptions;
287
+
288
+ if (domainCheck.isSubdomain) {
289
+ choicePrompt = `Root domain ${domainCheck.rootDomain} not found. What would you like to do?`;
290
+ choiceOptions = [
291
+ 'Choose a different service name with an available root domain',
292
+ 'View available domains in your account',
293
+ 'Cancel deployment (add root domain to Cloudflare first)'
294
+ ];
295
+ } else {
296
+ choicePrompt = `Ready to deploy new root domain ${domain}?`;
297
+ choiceOptions = [
298
+ 'Yes, proceed with first deployment',
299
+ 'Choose a different domain from available list',
300
+ 'Cancel deployment'
301
+ ];
302
+ }
303
+
304
+ const choice = await askChoice(choicePrompt, choiceOptions, 0);
305
+
306
+ switch (choice) {
307
+ case 0:
308
+ console.log(' ✅ Proceeding with first deployment');
309
+ return { status: 'new', action: 'deploy', services: [] };
310
+ case 1:
311
+ throw new Error('CHOOSE_DIFFERENT_DOMAIN');
312
+ case 2:
313
+ throw new Error('DEPLOYMENT_CANCELLED');
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Check for existing services on domain using orchestrator
319
+ */
320
+ async checkExistingServices(domain, domainCheck = null) {
321
+ console.log(' 🔍 Checking for existing services...');
322
+
323
+ try {
324
+ // Use orchestrator to get comprehensive service info
325
+ let domainServices = [];
326
+
327
+ try {
328
+ const portfolioStatus = await this.orchestrator.getPortfolioStatus();
329
+ const domainInfo = portfolioStatus.domainDetails?.find(d => d.domain === domain);
330
+
331
+ if (domainInfo && domainInfo.services) {
332
+ domainServices = domainInfo.services.map(s => ({
333
+ name: s.name,
334
+ status: s.status || 'unknown',
335
+ domain: domain,
336
+ lastDeployed: s.lastDeployed,
337
+ health: s.health
338
+ }));
339
+ }
340
+ } catch (orchestratorError) {
341
+ // Fallback to wrangler deployments
342
+ console.log(' 📋 Using fallback service discovery...');
343
+ try {
344
+ const deploymentsCmd = this.cmdConfig.getCloudflareCommand('deployments_list');
345
+ const { stdout } = await execAsync(deploymentsCmd);
346
+ const services = this.parseWranglerDeployments(stdout);
347
+ domainServices = services.filter(s => s.domain === domain);
348
+ } catch (fallbackError) {
349
+ console.log(' 📝 No deployment history found - treating as new domain');
350
+ domainServices = [];
351
+ }
352
+ }
353
+
354
+ if (domainServices.length === 0) {
355
+ console.log(' 📝 No existing services found - fresh deployment');
356
+ return { status: 'available', action: 'deploy', services: [] };
357
+ }
358
+
359
+ console.log(` 📋 Found ${domainServices.length} existing service(s):`);
360
+ domainServices.forEach(service => {
361
+ const healthInfo = service.health ? ` (${service.health})` : '';
362
+ const deployedInfo = service.lastDeployed ? ` - Last deployed: ${service.lastDeployed}` : '';
363
+ console.log(` - ${service.name} (${service.status})${healthInfo}${deployedInfo}`);
364
+ });
365
+
366
+ return await this.handleExistingServices(domain, domainServices);
367
+
368
+ } catch (error) {
369
+ console.log(` ⚠️ Could not check services: ${error.message}`);
370
+ return { status: 'unknown', action: 'deploy', services: [] };
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Handle existing services - ask for permission
376
+ */
377
+ async handleExistingServices(domain, services) {
378
+ const activeServices = services.filter(s => s.status === 'active' || s.status === 'deployed');
379
+
380
+ if (activeServices.length > 0) {
381
+ console.log(' ⚠️ Active services detected on this domain');
382
+
383
+ const updateConfirm = await askYesNo(
384
+ `Update/overwrite ${activeServices.length} active service(s) on ${domain}?`
385
+ );
386
+
387
+ if (!updateConfirm) {
388
+ throw new Error('DEPLOYMENT_CANCELLED - User declined to update active services');
389
+ }
390
+
391
+ console.log(' ✅ Permission granted to update existing services');
392
+ return { status: 'update', action: 'update', services: activeServices };
393
+ }
394
+
395
+ console.log(' ✅ Existing services are inactive - safe to deploy');
396
+ return { status: 'replace', action: 'deploy', services };
397
+ }
398
+
399
+ /**
400
+ * Parse wrangler deployments list output
401
+ */
402
+ parseWranglerDeployments(stdout) {
403
+ // Handle new wrangler deployments format
404
+ const lines = stdout.split('\n').filter(line => line.trim());
405
+ const services = [];
406
+
407
+ for (const line of lines) {
408
+ if (line.includes('worker') || line.includes('deployment')) {
409
+ // Extract service info from deployment output
410
+ const parts = line.split(/\s+/);
411
+ if (parts.length >= 2) {
412
+ services.push({
413
+ name: parts[0] || 'unknown',
414
+ status: 'deployed',
415
+ domain: this.extractDomainFromService(parts[0]),
416
+ lastDeployed: parts[1] || 'unknown'
417
+ });
418
+ }
419
+ }
420
+ }
421
+
422
+ return services;
423
+ }
424
+
425
+ /**
426
+ * Parse wrangler list output (legacy - keep for fallback)
427
+ */
428
+ parseWranglerList(stdout) {
429
+ const lines = stdout.split('\\n').filter(line => line.trim());
430
+ const services = [];
431
+
432
+ // Skip header lines
433
+ const dataLines = lines.slice(2);
434
+
435
+ for (const line of dataLines) {
436
+ if (line.trim()) {
437
+ const parts = line.split(/\\s+/);
438
+ if (parts.length >= 2) {
439
+ services.push({
440
+ name: parts[0],
441
+ status: parts[1] || 'unknown',
442
+ domain: this.extractDomainFromService(parts[0]),
443
+ });
444
+ }
445
+ }
446
+ }
447
+
448
+ return services;
449
+ }
450
+
451
+ /**
452
+ * Extract domain from service name (basic heuristic)
453
+ */
454
+ extractDomainFromService(serviceName) {
455
+ // Try to extract domain from service name patterns
456
+ // This is a heuristic - may need refinement based on naming conventions
457
+ if (serviceName.includes('.')) {
458
+ return serviceName.split('-')[0] || serviceName;
459
+ }
460
+ return null;
461
+ }
462
+
463
+ /**
464
+ * Parse account information from wrangler whoami output
465
+ */
466
+ parseAccountInfo(stdout) {
467
+ const accountInfo = {
468
+ email: null,
469
+ accountId: null,
470
+ accountName: null
471
+ };
472
+
473
+ const lines = stdout.split('\n');
474
+ let inAccountTable = false;
475
+
476
+ for (const line of lines) {
477
+ const trimmedLine = line.trim();
478
+
479
+ // Extract email from OAuth message
480
+ if (trimmedLine.includes('associated with the email')) {
481
+ const emailMatch = trimmedLine.match(/associated with the email (.+@.+?)\.?$/);
482
+ if (emailMatch) {
483
+ accountInfo.email = emailMatch[1].trim();
484
+ }
485
+ }
486
+
487
+ // Detect account table start
488
+ if (trimmedLine.includes('Account Name') && trimmedLine.includes('Account ID')) {
489
+ inAccountTable = true;
490
+ continue;
491
+ }
492
+
493
+ // Extract from account table
494
+ if (inAccountTable && trimmedLine.includes('│') && !trimmedLine.includes('Account Name')) {
495
+ // Stop at table end
496
+ if (trimmedLine.startsWith('└')) {
497
+ inAccountTable = false;
498
+ continue;
499
+ }
500
+
501
+ // Skip separator line
502
+ if (trimmedLine.includes('├') || trimmedLine.includes('─')) {
503
+ continue;
504
+ }
505
+
506
+ // Parse account data line: │ Account Name │ Account ID │
507
+ const parts = trimmedLine.split('│');
508
+ if (parts.length >= 3) {
509
+ const name = parts[1]?.trim();
510
+ const id = parts[2]?.trim();
511
+
512
+ if (name && name !== 'Account Name') {
513
+ accountInfo.accountName = name;
514
+ }
515
+
516
+ if (id && id !== 'Account ID' && id.length >= 30) {
517
+ accountInfo.accountId = id;
518
+ }
519
+ }
520
+ }
521
+ }
522
+
523
+ return accountInfo;
524
+ }
525
+
526
+ /**
527
+ * Extract root domain from service subdomain
528
+ * Handles cases where www.domain.com is the root, not domain.com
529
+ * e.g., data-service.greatidude.com -> check both www.greatidude.com AND greatidude.com
530
+ */
531
+ extractRootDomain(fullDomain) {
532
+ const parts = fullDomain.split('.');
533
+
534
+ if (parts.length >= 3) {
535
+ // For service subdomains like data-service.greatidude.com
536
+ // We need to check if www.greatidude.com exists (not just greatidude.com)
537
+ const domainWithoutService = parts.slice(1).join('.'); // greatidude.com
538
+ const wwwDomain = `www.${domainWithoutService}`; // www.greatidude.com
539
+
540
+ return {
541
+ bareRoot: domainWithoutService, // greatidude.com
542
+ wwwRoot: wwwDomain, // www.greatidude.com
543
+ domainsToCheck: [wwwDomain, domainWithoutService],
544
+ needsBothChecks: true
545
+ };
546
+ } else if (parts.length === 3 && parts[0] === 'www') {
547
+ // For www.greatidude.com - this IS the root domain
548
+ const bareRoot = parts.slice(1).join('.'); // greatidude.com
549
+ return {
550
+ bareRoot: bareRoot,
551
+ wwwRoot: fullDomain, // www.greatidude.com
552
+ domainsToCheck: [fullDomain, bareRoot],
553
+ isWwwRoot: true,
554
+ needsBothChecks: false
555
+ };
556
+ } else if (parts.length === 2) {
557
+ // For greatidude.com - check both bare and www
558
+ const wwwDomain = `www.${fullDomain}`;
559
+ return {
560
+ bareRoot: fullDomain, // greatidude.com
561
+ wwwRoot: wwwDomain, // www.greatidude.com
562
+ domainsToCheck: [wwwDomain, fullDomain],
563
+ needsBothChecks: true
564
+ };
565
+ }
566
+
567
+ return {
568
+ bareRoot: fullDomain,
569
+ wwwRoot: fullDomain,
570
+ domainsToCheck: [fullDomain],
571
+ needsBothChecks: false
572
+ };
573
+ }
574
+
575
+ /**
576
+ * Ensure API token is available for domain verification
577
+ */
578
+ async ensureApiToken() {
579
+ if (!this.apiToken) {
580
+ try {
581
+ console.log(' 🔑 API token required for domain verification');
582
+ this.apiToken = await this.tokenManager.getCloudflareToken();
583
+
584
+ // Also set it in domain discovery
585
+ this.domainDiscovery.apiToken = this.apiToken;
586
+
587
+ return true;
588
+ } catch (error) {
589
+ console.log(` ❌ Failed to get API token: ${error.message}`);
590
+ return false;
591
+ }
592
+ }
593
+ return true;
594
+ }
595
+
596
+ /**
597
+ * Check if domain exists in Cloudflare zones using domain discovery
598
+ * Handles both www.domain.com and domain.com as potential roots
599
+ */
600
+ async checkDomainInCloudflare(domain) {
601
+ if (!this.isAuthenticated) return false;
602
+
603
+ const rootInfo = this.extractRootDomain(domain);
604
+ const isServiceSubdomain = rootInfo.needsBothChecks || rootInfo.isWwwRoot === undefined;
605
+
606
+ console.log(` 🔍 Analyzing domain structure:`);
607
+ if (isServiceSubdomain && !rootInfo.isWwwRoot) {
608
+ console.log(` 📍 Service subdomain: ${domain}`);
609
+ console.log(` 🔍 Checking potential root domains:`);
610
+ console.log(` • www root: ${rootInfo.wwwRoot}`);
611
+ console.log(` • bare root: ${rootInfo.bareRoot}`);
612
+ } else if (rootInfo.isWwwRoot) {
613
+ console.log(` 🌐 WWW root domain: ${domain}`);
614
+ } else {
615
+ console.log(` 🌐 Domain: ${domain}`);
616
+ }
617
+
618
+ // Try to find which root domain exists
619
+ let foundDomain = null;
620
+ let domainConfig = null;
621
+
622
+ try {
623
+ // First try www version if it's different
624
+ if (rootInfo.wwwRoot !== rootInfo.bareRoot) {
625
+ try {
626
+ console.log(` 🔍 Checking: ${rootInfo.wwwRoot}...`);
627
+ domainConfig = await this.domainDiscovery.discoverDomainConfig(rootInfo.wwwRoot, this.apiToken);
628
+ if (domainConfig && domainConfig.zoneId) {
629
+ foundDomain = rootInfo.wwwRoot;
630
+ console.log(` ✅ Found www root: ${rootInfo.wwwRoot} (Zone: ${domainConfig.zoneId})`);
631
+ }
632
+ } catch (wwwError) {
633
+ if (wwwError.message.includes('API token is required')) {
634
+ console.log(` 🔑 API token required to verify ${rootInfo.wwwRoot}`);
635
+
636
+ // Automatically attempt to get API token
637
+ const tokenObtained = await this.ensureApiToken();
638
+ if (tokenObtained) {
639
+ console.log(` 🔄 Retrying www verification with API token...`);
640
+ try {
641
+ domainConfig = await this.domainDiscovery.discoverDomainConfig(rootInfo.wwwRoot, this.apiToken);
642
+ if (domainConfig && domainConfig.zoneId) {
643
+ foundDomain = rootInfo.wwwRoot;
644
+ console.log(` ✅ Found www root: ${rootInfo.wwwRoot} (Zone: ${domainConfig.zoneId})`);
645
+ }
646
+ } catch (retryError) {
647
+ console.log(` 📝 WWW root ${rootInfo.wwwRoot} not found after token verification`);
648
+ }
649
+ }
650
+ } else {
651
+ console.log(` 📝 WWW root ${rootInfo.wwwRoot} not found`);
652
+ }
653
+ }
654
+ }
655
+
656
+ // If www not found, try bare domain
657
+ if (!foundDomain) {
658
+ try {
659
+ console.log(` 🔍 Checking: ${rootInfo.bareRoot}...`);
660
+ domainConfig = await this.domainDiscovery.discoverDomainConfig(rootInfo.bareRoot, this.apiToken);
661
+ if (domainConfig && domainConfig.zoneId) {
662
+ foundDomain = rootInfo.bareRoot;
663
+ console.log(` ✅ Found bare root: ${rootInfo.bareRoot} (Zone: ${domainConfig.zoneId})`);
664
+ }
665
+ } catch (bareError) {
666
+ if (bareError.message.includes('API token is required')) {
667
+ console.log(` 🔑 API token required to verify ${rootInfo.bareRoot}`);
668
+
669
+ // Automatically attempt to get API token
670
+ const tokenObtained = await this.ensureApiToken();
671
+ if (tokenObtained) {
672
+ console.log(` 🔄 Retrying verification with API token...`);
673
+ try {
674
+ domainConfig = await this.domainDiscovery.discoverDomainConfig(rootInfo.bareRoot, this.apiToken);
675
+ if (domainConfig && domainConfig.zoneId) {
676
+ foundDomain = rootInfo.bareRoot;
677
+ console.log(` ✅ Found bare root: ${rootInfo.bareRoot} (Zone: ${domainConfig.zoneId})`);
678
+ }
679
+ } catch (retryError) {
680
+ console.log(` 📝 Bare root ${rootInfo.bareRoot} not found after token verification`);
681
+ }
682
+ }
683
+ } else {
684
+ console.log(` 📝 Bare root ${rootInfo.bareRoot} not found`);
685
+ }
686
+ }
687
+ }
688
+
689
+ if (foundDomain && domainConfig) {
690
+ if (isServiceSubdomain && !rootInfo.isWwwRoot) {
691
+ console.log(` 📋 Service ${domain} can be deployed to zone: ${foundDomain}`);
692
+ }
693
+
694
+ if (this.accountId) {
695
+ console.log(` 🔗 Checked in account: ${this.accountId}`);
696
+ }
697
+
698
+ return {
699
+ found: true,
700
+ rootDomain: foundDomain,
701
+ actualRootFound: foundDomain,
702
+ zoneId: domainConfig.zoneId,
703
+ isSubdomain: isServiceSubdomain,
704
+ serviceDomain: domain
705
+ };
706
+ }
707
+
708
+ // Neither root found - but this might be due to API token limitation
709
+ console.log(` ⚠️ Cannot verify domain via API (requires explicit API token)`);
710
+ console.log(` 🔍 OAuth authentication doesn't support direct domain verification`);
711
+ console.log(` 💡 Domains checked: ${rootInfo.wwwRoot}, ${rootInfo.bareRoot}`);
712
+ if (this.accountId) {
713
+ console.log(` 🔍 Account: ${this.accountId}`);
714
+ }
715
+
716
+ console.log(` 📋 To verify if domain exists:`);
717
+ console.log(` → Check Cloudflare Dashboard: https://dash.cloudflare.com`);
718
+ console.log(` → Look for ${rootInfo.bareRoot} or ${rootInfo.wwwRoot} in your domains`);
719
+
720
+ return {
721
+ found: false,
722
+ rootDomain: rootInfo.bareRoot,
723
+ alternateRoot: rootInfo.wwwRoot,
724
+ isSubdomain: isServiceSubdomain,
725
+ serviceDomain: domain,
726
+ requiresApiToken: true
727
+ };
728
+
729
+ } catch (error) {
730
+ console.log(` ❌ Error checking domains: ${error.message}`);
731
+ return {
732
+ found: false,
733
+ rootDomain: rootInfo.wwwRoot,
734
+ alternateRoot: rootInfo.bareRoot,
735
+ isSubdomain: isServiceSubdomain,
736
+ serviceDomain: domain,
737
+ error: error.message
738
+ };
739
+ }
740
+ }
741
+
742
+ /**
743
+ * Complete domain verification workflow
744
+ */
745
+ async verifyDomainWorkflow(requestedDomain) {
746
+ try {
747
+ // Step 1: Verify authentication
748
+ const authSuccess = await this.verifyAuthentication();
749
+
750
+ // Step 2: Get available domains (if authenticated)
751
+ if (authSuccess) {
752
+ await this.getAvailableDomains();
753
+ }
754
+
755
+ // Step 3: Verify domain and check services
756
+ const result = await this.verifyDomainAndMatchServices(requestedDomain);
757
+
758
+ return {
759
+ authenticated: this.isAuthenticated,
760
+ domainStatus: result.status,
761
+ recommendedAction: result.action,
762
+ existingServices: result.services,
763
+ availableDomains: this.availableDomains
764
+ };
765
+
766
+ } catch (error) {
767
+ if (error.message.includes('CHOOSE_DIFFERENT_DOMAIN')) {
768
+ return { action: 'choose_different', availableDomains: this.availableDomains };
769
+ }
770
+ throw error;
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Get comprehensive account details for troubleshooting
776
+ */
777
+ async getAccountDetails() {
778
+ if (!this.isAuthenticated) {
779
+ return { error: 'Not authenticated' };
780
+ }
781
+
782
+ try {
783
+ const details = {
784
+ accountId: this.accountId,
785
+ authenticated: this.isAuthenticated,
786
+ zones: [],
787
+ totalZones: 0
788
+ };
789
+
790
+ // Try to get zone information using the working API method
791
+ try {
792
+ const zones = await this.fetchAccountZonesViaAPI();
793
+ details.zones = zones.map(z => z.name);
794
+ details.totalZones = zones.length;
795
+ details.zoneListUnavailable = false;
796
+
797
+ } catch (zoneError) {
798
+ details.zoneError = 'Could not fetch zone information';
799
+ details.zoneListUnavailable = true;
800
+ }
801
+
802
+ return details;
803
+
804
+ } catch (error) {
805
+ return { error: error.message };
806
+ }
807
+ }
808
+
809
+ /**
810
+ * Parse zone list output for account context
811
+ * Falls back to alternative methods if direct zone listing not available
812
+ */
813
+ parseZoneList(stdout) {
814
+ const zones = [];
815
+ let totalZones = 0;
816
+
817
+ try {
818
+ // Check if zone listing is not available
819
+ if (stdout.includes('Zone listing not available') || stdout.includes('Unknown arguments')) {
820
+ return { zones: [], totalZones: 0, unavailable: true };
821
+ }
822
+
823
+ const lines = stdout.split('\n').filter(line => line.trim());
824
+
825
+ for (const line of lines) {
826
+ // Look for zone entries (domain names)
827
+ if (line.includes('.') && !line.includes('│') && !line.toLowerCase().includes('zone')) {
828
+ const parts = line.trim().split(/\s+/);
829
+ if (parts.length > 0 && parts[0].includes('.')) {
830
+ zones.push(parts[0]);
831
+ totalZones++;
832
+ }
833
+ }
834
+
835
+ // Also check for table format
836
+ if (line.includes('│') && line.includes('.')) {
837
+ const match = line.match(/│\s*([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\s*│/);
838
+ if (match && !zones.includes(match[1])) {
839
+ zones.push(match[1]);
840
+ totalZones++;
841
+ }
842
+ }
843
+ }
844
+
845
+ } catch (error) {
846
+ // Ignore parsing errors
847
+ }
848
+
849
+ return { zones: zones.slice(0, 5), totalZones }; // Limit to first 5 zones
850
+ }
851
+
852
+ /**
853
+ * Fetch account zones using Cloudflare API (the working method!)
854
+ */
855
+ async fetchAccountZonesViaAPI() {
856
+ if (!this.accountId) {
857
+ throw new Error('Account ID not available');
858
+ }
859
+
860
+ // Use the domain discovery module's API method since it works
861
+ try {
862
+ const zones = await this.domainDiscovery.fetchAccountZones(this.accountId, this.apiToken);
863
+ return zones || [];
864
+ } catch (error) {
865
+ // Fallback: try without explicit API token (use wrangler's auth)
866
+ try {
867
+ const response = await this.domainDiscovery.makeCloudflareRequest(
868
+ `https://api.cloudflare.com/client/v4/zones?account.id=${this.accountId}`,
869
+ null // Let it use default auth
870
+ );
871
+
872
+ if (response.success) {
873
+ return response.result || [];
874
+ }
875
+ } catch (fallbackError) {
876
+ // If all else fails, return empty array
877
+ }
878
+
879
+ throw error;
880
+ }
881
+ }
882
+
883
+ /**
884
+ * Show what domains are actually available in the Cloudflare account
885
+ */
886
+ async showAvailableDomainsInAccount() {
887
+ console.log('');
888
+ console.log(' 📋 Domains found in your Cloudflare account:');
889
+
890
+ const accountDetails = await this.getAccountDetails();
891
+
892
+ if (accountDetails.error) {
893
+ console.log(' ⚠️ Could not fetch account domains');
894
+ return;
895
+ }
896
+
897
+ if (accountDetails.zoneListUnavailable && accountDetails.zoneError) {
898
+ console.log(' ⚠️ API-based domain listing requires explicit API token');
899
+ console.log(' 🔍 Using OAuth authentication - cannot directly list zones');
900
+ console.log(' 💡 To check your domains:');
901
+ console.log(' → Visit Cloudflare Dashboard: https://dash.cloudflare.com');
902
+ console.log(' → Check the "Websites" section for your domains');
903
+ console.log(' 📧 Account: ' + (this.accountId || 'Unknown'));
904
+ } else if (accountDetails.totalZones === 0) {
905
+ console.log(' 📝 No domains found in this Cloudflare account');
906
+ console.log(' 💡 This account has no DNS zones configured');
907
+ console.log(' 🌐 To add a domain:');
908
+ console.log(' → Go to Cloudflare Dashboard → Add a Site');
909
+ } else {
910
+ console.log(` 🌐 Found ${accountDetails.totalZones} domain(s):`);
911
+
912
+ if (accountDetails.zones && accountDetails.zones.length > 0) {
913
+ accountDetails.zones.forEach((zone, index) => {
914
+ console.log(` ${index + 1}. ${zone}`);
915
+ });
916
+
917
+ if (accountDetails.totalZones > accountDetails.zones.length) {
918
+ console.log(` ... and ${accountDetails.totalZones - accountDetails.zones.length} more`);
919
+ }
920
+ }
921
+ }
922
+ }
923
+
924
+ /**
925
+ * Show actionable domain suggestions based on account status
926
+ */
927
+ async showDomainSuggestions(serviceDomain, domainCheck) {
928
+ console.log(' 💡 Solutions:');
929
+
930
+ const accountDetails = await this.getAccountDetails();
931
+
932
+ if (accountDetails.totalZones > 0) {
933
+ console.log(` 1. 🔄 Use an existing domain from your account:`);
934
+ accountDetails.zones.forEach(zone => {
935
+ const suggestedService = serviceDomain.replace(domainCheck.rootDomain, zone);
936
+ console.log(` → ${suggestedService}`);
937
+ });
938
+ console.log(` 2. ➕ Add ${domainCheck.rootDomain} to your Cloudflare account`);
939
+ console.log(` 3. 🌐 Transfer ${domainCheck.rootDomain} DNS to Cloudflare`);
940
+ } else {
941
+ console.log(` 1. ➕ Add ${domainCheck.rootDomain} to your Cloudflare account first`);
942
+ console.log(` 2. 🌐 Set up DNS for ${domainCheck.rootDomain} in Cloudflare`);
943
+ console.log(` 3. 🔄 Or use a different domain that you own`);
944
+ }
945
+
946
+ console.log('');
947
+ console.log(' 📚 How to add a domain to Cloudflare:');
948
+ console.log(' → Go to Cloudflare Dashboard → Add Site → Enter your domain');
949
+ console.log(' → Update nameservers at your domain registrar');
950
+ console.log(' → Wait for DNS propagation (usually 24-48 hours)');
951
+ }
952
+ }