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