@tamyla/clodo-framework 3.1.8 → 3.1.10
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.
- package/CHANGELOG.md +15 -0
- package/bin/database/enterprise-db-manager.js +4 -4
- package/bin/security/security-cli.js +2 -2
- package/bin/service-management/create-service.js +2 -2
- package/bin/service-management/init-service.js +2 -2
- package/bin/shared/cloudflare/domain-manager.js +1 -1
- package/bin/shared/config/index.js +1 -1
- package/bin/shared/deployment/index.js +2 -2
- package/dist/bin/clodo-service-old.js +868 -0
- package/dist/bin/clodo-service-test.js +10 -0
- package/dist/bin/clodo-service.js +62 -0
- package/dist/bin/commands/assess.js +76 -0
- package/dist/bin/commands/create.js +56 -0
- package/dist/bin/commands/deploy.js +196 -0
- package/dist/bin/commands/diagnose.js +70 -0
- package/dist/bin/commands/helpers.js +138 -0
- package/dist/bin/commands/update.js +55 -0
- package/dist/bin/commands/validate.js +26 -0
- package/dist/bin/database/deployment-db-manager.js +423 -0
- package/dist/bin/database/enterprise-db-manager.js +457 -0
- package/dist/bin/database/wrangler-d1-manager.js +685 -0
- package/dist/bin/deployment/enterprise-deploy.js +877 -0
- package/dist/bin/deployment/master-deploy.js +1376 -0
- package/dist/bin/deployment/modular-enterprise-deploy.js +466 -0
- package/dist/bin/deployment/modules/DeploymentConfiguration.js +395 -0
- package/dist/bin/deployment/modules/DeploymentOrchestrator.js +492 -0
- package/dist/bin/deployment/modules/EnvironmentManager.js +517 -0
- package/dist/bin/deployment/modules/MonitoringIntegration.js +560 -0
- package/dist/bin/deployment/modules/ValidationManager.js +342 -0
- package/dist/bin/deployment/orchestration/BaseDeploymentOrchestrator.js +426 -0
- package/dist/bin/deployment/orchestration/EnterpriseOrchestrator.js +401 -0
- package/dist/bin/deployment/orchestration/PortfolioOrchestrator.js +273 -0
- package/dist/bin/deployment/orchestration/SingleServiceOrchestrator.js +231 -0
- package/dist/bin/deployment/orchestration/UnifiedDeploymentOrchestrator.js +662 -0
- package/dist/bin/deployment/test-interactive-utils.js +66 -0
- package/dist/bin/portfolio/portfolio-manager.js +487 -0
- package/dist/bin/security/security-cli.js +108 -0
- package/dist/bin/service-management/create-service.js +122 -0
- package/dist/bin/service-management/init-service.js +79 -0
- package/dist/{shared → bin/shared}/cloudflare/domain-manager.js +1 -1
- package/dist/{shared → bin/shared}/config/index.js +1 -1
- package/dist/bin/shared/deployment/index.js +10 -0
- package/dist/deployment/index.js +10 -9
- package/dist/deployment/rollback-manager.js +21 -508
- package/package.json +7 -7
- package/dist/shared/deployment/auditor.js +0 -986
- package/dist/shared/deployment/index.js +0 -10
- package/dist/shared/deployment/validator.js +0 -670
- package/dist/shared/production-tester/api-tester.js +0 -80
- package/dist/shared/production-tester/auth-tester.js +0 -129
- package/dist/shared/production-tester/core.js +0 -217
- package/dist/shared/production-tester/database-tester.js +0 -105
- package/dist/shared/production-tester/index.js +0 -74
- package/dist/shared/production-tester/load-tester.js +0 -120
- package/dist/shared/production-tester/performance-tester.js +0 -105
- /package/dist/{shared → bin/shared}/cloudflare/domain-discovery.js +0 -0
- /package/dist/{shared → bin/shared}/cloudflare/index.js +0 -0
- /package/dist/{shared → bin/shared}/cloudflare/ops.js +0 -0
- /package/dist/{shared → bin/shared}/config/ConfigurationManager.js +0 -0
- /package/dist/{shared → bin/shared}/config/cache.js +0 -0
- /package/dist/{shared → bin/shared}/config/command-config-manager.js +0 -0
- /package/dist/{shared → bin/shared}/config/manager.js +0 -0
- /package/dist/{shared → bin/shared}/database/connection-manager.js +0 -0
- /package/dist/{shared → bin/shared}/database/index.js +0 -0
- /package/dist/{shared → bin/shared}/database/orchestrator.js +0 -0
- /package/dist/{deployment → bin/shared/deployment}/auditor.js +0 -0
- /package/dist/{shared → bin/shared}/deployment/rollback-manager.js +0 -0
- /package/dist/{deployment → bin/shared/deployment}/validator.js +0 -0
- /package/dist/{shared → bin/shared}/index.js +0 -0
- /package/dist/{shared → bin/shared}/logging/Logger.js +0 -0
- /package/dist/{shared → bin/shared}/monitoring/health-checker.js +0 -0
- /package/dist/{shared → bin/shared}/monitoring/index.js +0 -0
- /package/dist/{shared → bin/shared}/monitoring/memory-manager.js +0 -0
- /package/dist/{shared → bin/shared}/monitoring/production-monitor.js +0 -0
- /package/dist/{deployment/testers → bin/shared/production-tester}/api-tester.js +0 -0
- /package/dist/{deployment/testers → bin/shared/production-tester}/auth-tester.js +0 -0
- /package/dist/{deployment/testers → bin/shared/production-tester}/core.js +0 -0
- /package/dist/{deployment/testers → bin/shared/production-tester}/database-tester.js +0 -0
- /package/dist/{deployment/testers → bin/shared/production-tester}/index.js +0 -0
- /package/dist/{deployment/testers → bin/shared/production-tester}/load-tester.js +0 -0
- /package/dist/{deployment/testers → bin/shared/production-tester}/performance-tester.js +0 -0
- /package/dist/{shared → bin/shared}/security/api-token-manager.js +0 -0
- /package/dist/{shared → bin/shared}/security/index.js +0 -0
- /package/dist/{shared → bin/shared}/security/secret-generator.js +0 -0
- /package/dist/{shared → bin/shared}/security/secure-token-manager.js +0 -0
- /package/dist/{shared → bin/shared}/utils/ErrorHandler.js +0 -0
- /package/dist/{shared → bin/shared}/utils/error-recovery.js +0 -0
- /package/dist/{shared → bin/shared}/utils/file-manager.js +0 -0
- /package/dist/{shared → bin/shared}/utils/formatters.js +0 -0
- /package/dist/{shared → bin/shared}/utils/graceful-shutdown-manager.js +0 -0
- /package/dist/{shared → bin/shared}/utils/index.js +0 -0
- /package/dist/{shared → bin/shared}/utils/interactive-prompts.js +0 -0
- /package/dist/{shared → bin/shared}/utils/interactive-utils.js +0 -0
- /package/dist/{shared → bin/shared}/utils/rate-limiter.js +0 -0
- /package/dist/{shared → bin/shared}/validation/ValidationRegistry.js +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
const program = new Command();
|
|
3
|
+
program.name('clodo-service-test').description('Test Clodo Framework CLI').version('1.0.0');
|
|
4
|
+
program.command('hello').description('Say hello').action(() => {
|
|
5
|
+
console.log('Hello from Clodo Service CLI!');
|
|
6
|
+
});
|
|
7
|
+
program.command('create').description('Create a new service').action(() => {
|
|
8
|
+
console.log('Create command would run here...');
|
|
9
|
+
});
|
|
10
|
+
program.parse();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Clodo Framework - Unified Service Management CLI
|
|
5
|
+
*
|
|
6
|
+
* Main entry point that registers and orchestrates all service management commands.
|
|
7
|
+
* Each command is in its own module in bin/commands/ for clean separation of concerns.
|
|
8
|
+
*
|
|
9
|
+
* Commands:
|
|
10
|
+
* - create Create a new Clodo service with conversational setup
|
|
11
|
+
* - deploy Deploy a Clodo service with smart credential handling
|
|
12
|
+
* - validate Validate an existing service configuration
|
|
13
|
+
* - update Update an existing service configuration
|
|
14
|
+
* - diagnose Diagnose and report issues with an existing service
|
|
15
|
+
* - assess Run intelligent capability assessment
|
|
16
|
+
* - list-types List available service types and their features
|
|
17
|
+
*/
|
|
18
|
+
import { Command } from 'commander';
|
|
19
|
+
import chalk from 'chalk';
|
|
20
|
+
|
|
21
|
+
// Import command registration functions
|
|
22
|
+
import { registerCreateCommand } from './commands/create.js';
|
|
23
|
+
import { registerDeployCommand } from './commands/deploy.js';
|
|
24
|
+
import { registerValidateCommand } from './commands/validate.js';
|
|
25
|
+
import { registerUpdateCommand } from './commands/update.js';
|
|
26
|
+
import { registerDiagnoseCommand } from './commands/diagnose.js';
|
|
27
|
+
import { registerAssessCommand } from './commands/assess.js';
|
|
28
|
+
|
|
29
|
+
// Create program instance
|
|
30
|
+
const program = new Command();
|
|
31
|
+
program.name('clodo-service').description('Unified conversational CLI for Clodo Framework service lifecycle management').version('1.0.0');
|
|
32
|
+
|
|
33
|
+
// Register all command modules
|
|
34
|
+
registerCreateCommand(program);
|
|
35
|
+
registerDeployCommand(program);
|
|
36
|
+
registerValidateCommand(program);
|
|
37
|
+
registerUpdateCommand(program);
|
|
38
|
+
registerDiagnoseCommand(program);
|
|
39
|
+
registerAssessCommand(program);
|
|
40
|
+
|
|
41
|
+
// List available service types
|
|
42
|
+
program.command('list-types').description('List available service types and their features').action(() => {
|
|
43
|
+
console.log(chalk.cyan('Available Clodo Framework Service Types:'));
|
|
44
|
+
console.log('');
|
|
45
|
+
const types = {
|
|
46
|
+
'data-service': ['Authentication', 'Authorization', 'File Storage', 'Search', 'Filtering', 'Pagination'],
|
|
47
|
+
'auth-service': ['Authentication', 'Authorization', 'User Profiles', 'Email Notifications', 'Magic Link Auth'],
|
|
48
|
+
'content-service': ['File Storage', 'Search', 'Filtering', 'Pagination', 'Caching'],
|
|
49
|
+
'api-gateway': ['Authentication', 'Authorization', 'Rate Limiting', 'Caching', 'Monitoring'],
|
|
50
|
+
'generic': ['Logging', 'Monitoring', 'Error Reporting']
|
|
51
|
+
};
|
|
52
|
+
Object.entries(types).forEach(([type, features]) => {
|
|
53
|
+
console.log(chalk.green(` ${type}`));
|
|
54
|
+
features.forEach(feature => {
|
|
55
|
+
console.log(chalk.white(` • ${feature}`));
|
|
56
|
+
});
|
|
57
|
+
console.log('');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Parse command line arguments
|
|
62
|
+
program.parse();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assess Command - Run intelligent capability assessment
|
|
3
|
+
* Requires @tamyla/clodo-orchestration package for professional edition
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
export function registerAssessCommand(program) {
|
|
8
|
+
program.command('assess [service-path]').description('Run intelligent capability assessment (requires @tamyla/clodo-orchestration)').option('--export <file>', 'Export assessment results to JSON file').option('--domain <domain>', 'Domain name for assessment').option('--service-type <type>', 'Service type for assessment').option('--token <token>', 'Cloudflare API token').action(async (servicePath, options) => {
|
|
9
|
+
try {
|
|
10
|
+
// Try to load professional orchestration package
|
|
11
|
+
let orchestrationModule;
|
|
12
|
+
try {
|
|
13
|
+
orchestrationModule = await import('@tamyla/clodo-orchestration');
|
|
14
|
+
} catch (err) {
|
|
15
|
+
console.error(chalk.red('❌ clodo-orchestration package not found'));
|
|
16
|
+
console.error(chalk.yellow('💡 Install with: npm install @tamyla/clodo-orchestration'));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const {
|
|
20
|
+
CapabilityAssessmentEngine,
|
|
21
|
+
ServiceAutoDiscovery,
|
|
22
|
+
runAssessmentWorkflow
|
|
23
|
+
} = orchestrationModule;
|
|
24
|
+
const targetPath = servicePath || process.cwd();
|
|
25
|
+
console.log(chalk.cyan('\n🧠 Professional Capability Assessment'));
|
|
26
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
27
|
+
console.log(chalk.white(`Service Path: ${targetPath}`));
|
|
28
|
+
if (options.domain) {
|
|
29
|
+
console.log(chalk.white(`Domain: ${options.domain}`));
|
|
30
|
+
}
|
|
31
|
+
if (options.serviceType) {
|
|
32
|
+
console.log(chalk.white(`Service Type: ${options.serviceType}`));
|
|
33
|
+
}
|
|
34
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
35
|
+
|
|
36
|
+
// Use the assessment workflow
|
|
37
|
+
const assessment = await runAssessmentWorkflow({
|
|
38
|
+
servicePath: targetPath,
|
|
39
|
+
domain: options.domain,
|
|
40
|
+
serviceType: options.serviceType,
|
|
41
|
+
token: options.token || process.env.CLOUDFLARE_API_TOKEN
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Display results
|
|
45
|
+
console.log(chalk.cyan('\n✅ Assessment Results'));
|
|
46
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
47
|
+
console.log(chalk.white(`Service Type: ${assessment.mergedInputs?.serviceType || assessment.serviceType || 'Not determined'}`));
|
|
48
|
+
console.log(chalk.white(`Confidence: ${assessment.confidence}%`));
|
|
49
|
+
if (assessment.gapAnalysis?.missing) {
|
|
50
|
+
console.log(chalk.white(`Missing Capabilities: ${assessment.gapAnalysis.missing.length}`));
|
|
51
|
+
if (assessment.gapAnalysis.missing.length > 0) {
|
|
52
|
+
console.log(chalk.yellow('\n⚠️ Missing:'));
|
|
53
|
+
assessment.gapAnalysis.missing.forEach(gap => {
|
|
54
|
+
console.log(chalk.yellow(` • ${gap.capability}: ${gap.reason || 'Not available'}`));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
59
|
+
|
|
60
|
+
// Export results if requested
|
|
61
|
+
if (options.export) {
|
|
62
|
+
const {
|
|
63
|
+
writeFileSync
|
|
64
|
+
} = await import('fs');
|
|
65
|
+
writeFileSync(options.export, JSON.stringify(assessment, null, 2));
|
|
66
|
+
console.log(chalk.green(`\n📄 Results exported to: ${options.export}`));
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(chalk.red(`Assessment failed: ${error.message}`));
|
|
70
|
+
if (process.env.DEBUG) {
|
|
71
|
+
console.error(chalk.gray(error.stack));
|
|
72
|
+
}
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create Command - Create a new Clodo service with conversational setup
|
|
3
|
+
*
|
|
4
|
+
* Input Strategy: FULL three-tier collection (88 fields)
|
|
5
|
+
* Uses ServiceOrchestrator to collect all required information interactively
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { ServiceOrchestrator } from '../../src/service-management/ServiceOrchestrator.js';
|
|
10
|
+
export function registerCreateCommand(program) {
|
|
11
|
+
program.command('create').description('Create a new Clodo service with conversational setup').option('-n, --non-interactive', 'Run in non-interactive mode with all required parameters').option('--service-name <name>', 'Service name (required in non-interactive mode)').option('--service-type <type>', 'Service type: data-service, auth-service, content-service, api-gateway, generic', 'generic').option('--domain-name <domain>', 'Domain name (required in non-interactive mode)').option('--cloudflare-token <token>', 'Cloudflare API token (required in non-interactive mode)').option('--cloudflare-account-id <id>', 'Cloudflare account ID (required in non-interactive mode)').option('--cloudflare-zone-id <id>', 'Cloudflare zone ID (required in non-interactive mode)').option('--environment <env>', 'Target environment: development, staging, production', 'development').option('--output-path <path>', 'Output directory for generated service', '.').option('--template-path <path>', 'Path to service templates', './templates').action(async options => {
|
|
12
|
+
try {
|
|
13
|
+
const orchestrator = new ServiceOrchestrator({
|
|
14
|
+
interactive: !options.nonInteractive,
|
|
15
|
+
outputPath: options.outputPath,
|
|
16
|
+
templatePath: options.templatePath
|
|
17
|
+
});
|
|
18
|
+
if (options.nonInteractive) {
|
|
19
|
+
// Validate required parameters for non-interactive mode
|
|
20
|
+
const required = ['serviceName', 'domainName', 'cloudflareToken', 'cloudflareAccountId', 'cloudflareZoneId'];
|
|
21
|
+
const missing = required.filter(key => !options[key]);
|
|
22
|
+
if (missing.length > 0) {
|
|
23
|
+
console.error(chalk.red(`Missing required parameters: ${missing.join(', ')}`));
|
|
24
|
+
console.error(chalk.yellow('Use --help for parameter details'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Convert CLI options to core inputs
|
|
29
|
+
const coreInputs = {
|
|
30
|
+
serviceName: options.serviceName,
|
|
31
|
+
serviceType: options.serviceType,
|
|
32
|
+
domainName: options.domainName,
|
|
33
|
+
cloudflareToken: options.cloudflareToken,
|
|
34
|
+
cloudflareAccountId: options.cloudflareAccountId,
|
|
35
|
+
cloudflareZoneId: options.cloudflareZoneId,
|
|
36
|
+
environment: options.environment
|
|
37
|
+
};
|
|
38
|
+
await orchestrator.runNonInteractive(coreInputs);
|
|
39
|
+
} else {
|
|
40
|
+
await orchestrator.runInteractive();
|
|
41
|
+
}
|
|
42
|
+
console.log(chalk.green('\n✓ Service creation completed successfully!'));
|
|
43
|
+
console.log(chalk.cyan('Next steps:'));
|
|
44
|
+
console.log(chalk.white(' 1. cd into your new service directory'));
|
|
45
|
+
console.log(chalk.white(' 2. Run npm install'));
|
|
46
|
+
console.log(chalk.white(' 3. Configure additional settings in src/config/domains.js'));
|
|
47
|
+
console.log(chalk.white(' 4. Run npm run deploy to deploy to Cloudflare'));
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(chalk.red(`\n✗ Service creation failed: ${error.message}`));
|
|
50
|
+
if (error.details) {
|
|
51
|
+
console.error(chalk.yellow(`Details: ${error.details}`));
|
|
52
|
+
}
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy Command - Smart minimal input deployment with service detection
|
|
3
|
+
*
|
|
4
|
+
* Input Strategy: SMART MINIMAL
|
|
5
|
+
* - Detects if project is a service (clodo-service-manifest.json)
|
|
6
|
+
* - Gathers credentials smartly: env vars → flags → fail with helpful message (never prompt)
|
|
7
|
+
* - Integrates with modular-enterprise-deploy.js for clean CLI-based deployment
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { existsSync, readFileSync } from 'fs';
|
|
12
|
+
import { resolve, join } from 'path';
|
|
13
|
+
export function registerDeployCommand(program) {
|
|
14
|
+
program.command('deploy').description('Deploy a Clodo service with smart credential handling').option('--token <token>', 'Cloudflare API token').option('--account-id <id>', 'Cloudflare account ID').option('--zone-id <id>', 'Cloudflare zone ID').option('--dry-run', 'Simulate deployment without making changes').option('--quiet', 'Quiet mode - minimal output').option('--service-path <path>', 'Path to service directory', '.').action(async options => {
|
|
15
|
+
try {
|
|
16
|
+
console.log(chalk.cyan('\n🚀 Clodo Service Deployment\n'));
|
|
17
|
+
|
|
18
|
+
// Step 1: Detect if this is a service project
|
|
19
|
+
const servicePath = resolve(options.servicePath);
|
|
20
|
+
const manifestPath = join(servicePath, 'clodo-service-manifest.json');
|
|
21
|
+
if (!existsSync(manifestPath)) {
|
|
22
|
+
console.error(chalk.red('❌ This is not a Clodo service project'));
|
|
23
|
+
console.error(chalk.yellow('\nExpected to find: clodo-service-manifest.json'));
|
|
24
|
+
console.error(chalk.cyan('\nAre you trying to deploy the framework itself?'));
|
|
25
|
+
console.error(chalk.white('The clodo-framework repository is a library, not deployable.'));
|
|
26
|
+
console.error(chalk.white('Create a service first: npx clodo-service create'));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Read service manifest
|
|
31
|
+
let manifest;
|
|
32
|
+
try {
|
|
33
|
+
const manifestContent = readFileSync(manifestPath, 'utf8');
|
|
34
|
+
manifest = JSON.parse(manifestContent);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(chalk.red('❌ Failed to read service manifest'));
|
|
37
|
+
console.error(chalk.yellow(`Error: ${err.message}`));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const serviceName = manifest.serviceName || 'unknown-service';
|
|
41
|
+
const serviceType = manifest.serviceType || 'unknown-type';
|
|
42
|
+
console.log(chalk.white(`Service: ${chalk.bold(serviceName)}`));
|
|
43
|
+
console.log(chalk.white(`Type: ${serviceType}`));
|
|
44
|
+
console.log(chalk.white(`Path: ${servicePath}\n`));
|
|
45
|
+
|
|
46
|
+
// Step 2: Smart credential gathering
|
|
47
|
+
// Priority: flags → environment variables → fail with helpful message
|
|
48
|
+
const credentials = {
|
|
49
|
+
token: options.token || process.env.CLOUDFLARE_API_TOKEN,
|
|
50
|
+
accountId: options.accountId || process.env.CLOUDFLARE_ACCOUNT_ID,
|
|
51
|
+
zoneId: options.zoneId || process.env.CLOUDFLARE_ZONE_ID
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Check for missing credentials
|
|
55
|
+
const missing = [];
|
|
56
|
+
if (!credentials.token) missing.push('CLOUDFLARE_API_TOKEN');
|
|
57
|
+
if (!credentials.accountId) missing.push('CLOUDFLARE_ACCOUNT_ID');
|
|
58
|
+
if (!credentials.zoneId) missing.push('CLOUDFLARE_ZONE_ID');
|
|
59
|
+
if (missing.length > 0) {
|
|
60
|
+
console.error(chalk.red('❌ Missing required Cloudflare credentials\n'));
|
|
61
|
+
console.error(chalk.white('Please provide via:'));
|
|
62
|
+
console.error(chalk.cyan(' Environment Variables:'));
|
|
63
|
+
missing.forEach(key => {
|
|
64
|
+
console.error(chalk.white(` export ${key}=<your-${key.toLowerCase()}>`));
|
|
65
|
+
});
|
|
66
|
+
console.error(chalk.cyan('\n Command Flags:'));
|
|
67
|
+
if (missing.includes('CLOUDFLARE_API_TOKEN')) {
|
|
68
|
+
console.error(chalk.white(' --token <token>'));
|
|
69
|
+
}
|
|
70
|
+
if (missing.includes('CLOUDFLARE_ACCOUNT_ID')) {
|
|
71
|
+
console.error(chalk.white(' --account-id <id>'));
|
|
72
|
+
}
|
|
73
|
+
if (missing.includes('CLOUDFLARE_ZONE_ID')) {
|
|
74
|
+
console.error(chalk.white(' --zone-id <id>'));
|
|
75
|
+
}
|
|
76
|
+
console.error(chalk.cyan('\n Example:'));
|
|
77
|
+
console.error(chalk.white(' npx clodo-service deploy --token abc123 --account-id xyz789 --zone-id def456'));
|
|
78
|
+
console.error(chalk.white(' OR'));
|
|
79
|
+
console.error(chalk.white(' export CLOUDFLARE_API_TOKEN=abc123'));
|
|
80
|
+
console.error(chalk.white(' export CLOUDFLARE_ACCOUNT_ID=xyz789'));
|
|
81
|
+
console.error(chalk.white(' export CLOUDFLARE_ZONE_ID=def456'));
|
|
82
|
+
console.error(chalk.white(' npx clodo-service deploy\n'));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Step 3: Extract configuration from manifest
|
|
87
|
+
const config = manifest.configuration || {};
|
|
88
|
+
const domain = config.domain || config.domainName;
|
|
89
|
+
if (!domain) {
|
|
90
|
+
console.error(chalk.red('❌ No domain configured in service manifest'));
|
|
91
|
+
console.error(chalk.yellow('Update clodo-service-manifest.json with domain configuration'));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
console.log(chalk.cyan('📋 Deployment Plan:'));
|
|
95
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
96
|
+
console.log(chalk.white(`Service: ${serviceName}`));
|
|
97
|
+
console.log(chalk.white(`Type: ${serviceType}`));
|
|
98
|
+
console.log(chalk.white(`Domain: ${domain}`));
|
|
99
|
+
console.log(chalk.white(`Account: ${credentials.accountId.substring(0, 8)}...`));
|
|
100
|
+
console.log(chalk.white(`Zone: ${credentials.zoneId.substring(0, 8)}...`));
|
|
101
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
102
|
+
if (options.dryRun) {
|
|
103
|
+
console.log(chalk.yellow('\n🔍 DRY RUN MODE - No changes will be made\n'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Step 4: Load and call modular deployer
|
|
107
|
+
console.log(chalk.cyan('\n⚙️ Initializing deployment system...\n'));
|
|
108
|
+
let ModularEnterpriseDeployer;
|
|
109
|
+
try {
|
|
110
|
+
const module = await import('../../deployment/modular-enterprise-deploy.js');
|
|
111
|
+
ModularEnterpriseDeployer = module.ModularEnterpriseDeployer || module.default;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(chalk.red('❌ Failed to load deployment system'));
|
|
114
|
+
console.error(chalk.yellow(`Error: ${err.message}`));
|
|
115
|
+
if (process.env.DEBUG) {
|
|
116
|
+
console.error(chalk.gray(err.stack));
|
|
117
|
+
}
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
if (!ModularEnterpriseDeployer) {
|
|
121
|
+
console.error(chalk.red('❌ ModularEnterpriseDeployer not found in deployment module'));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create deployer instance with gathered credentials
|
|
126
|
+
const deployer = new ModularEnterpriseDeployer({
|
|
127
|
+
apiToken: credentials.token,
|
|
128
|
+
accountId: credentials.accountId,
|
|
129
|
+
zoneId: credentials.zoneId,
|
|
130
|
+
domain: domain,
|
|
131
|
+
dryRun: options.dryRun,
|
|
132
|
+
quiet: options.quiet,
|
|
133
|
+
servicePath: servicePath,
|
|
134
|
+
serviceName: serviceName,
|
|
135
|
+
serviceType: serviceType
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Run deployment
|
|
139
|
+
console.log(chalk.cyan('🚀 Starting deployment...\n'));
|
|
140
|
+
const result = await deployer.run({
|
|
141
|
+
manifest: manifest,
|
|
142
|
+
credentials: credentials,
|
|
143
|
+
dryRun: options.dryRun
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Display results
|
|
147
|
+
console.log(chalk.green('\n✅ Deployment Completed Successfully!\n'));
|
|
148
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
149
|
+
if (result.url) {
|
|
150
|
+
console.log(chalk.white(`🌐 Service URL: ${chalk.bold(result.url)}`));
|
|
151
|
+
}
|
|
152
|
+
console.log(chalk.white(`📦 Service: ${serviceName}`));
|
|
153
|
+
console.log(chalk.white(`🔧 Type: ${serviceType}`));
|
|
154
|
+
console.log(chalk.white(`🌍 Domain: ${domain}`));
|
|
155
|
+
if (result.workerId) {
|
|
156
|
+
console.log(chalk.white(`👤 Worker ID: ${result.workerId}`));
|
|
157
|
+
}
|
|
158
|
+
if (result.status) {
|
|
159
|
+
const statusColor = result.status.toLowerCase().includes('success') ? chalk.green : chalk.yellow;
|
|
160
|
+
console.log(chalk.white(`📊 Status: ${statusColor(result.status)}`));
|
|
161
|
+
}
|
|
162
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
163
|
+
|
|
164
|
+
// Next steps
|
|
165
|
+
if (!options.dryRun) {
|
|
166
|
+
console.log(chalk.cyan('\n💡 Next Steps:'));
|
|
167
|
+
console.log(chalk.white(' • Test deployment: curl ' + (result.url || `https://${domain}`)));
|
|
168
|
+
console.log(chalk.white(' • View logs: wrangler tail ' + serviceName));
|
|
169
|
+
console.log(chalk.white(' • Monitor: https://dash.cloudflare.com'));
|
|
170
|
+
}
|
|
171
|
+
if (process.env.DEBUG && result.details) {
|
|
172
|
+
console.log(chalk.gray('\n📋 Full Result:'));
|
|
173
|
+
console.log(chalk.gray(JSON.stringify(result, null, 2)));
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error(chalk.red(`\n❌ Deployment failed: ${error.message}`));
|
|
177
|
+
if (error.message.includes('credentials') || error.message.includes('auth')) {
|
|
178
|
+
console.error(chalk.yellow('\n💡 Credential Issue:'));
|
|
179
|
+
console.error(chalk.white(' Check your API token, account ID, and zone ID'));
|
|
180
|
+
console.error(chalk.white(' Visit: https://dash.cloudflare.com/profile/api-tokens'));
|
|
181
|
+
}
|
|
182
|
+
if (error.message.includes('domain')) {
|
|
183
|
+
console.error(chalk.yellow('\n💡 Domain Issue:'));
|
|
184
|
+
console.error(chalk.white(' Verify domain exists in Cloudflare'));
|
|
185
|
+
console.error(chalk.white(' Check API token has zone:read permissions'));
|
|
186
|
+
}
|
|
187
|
+
if (process.env.DEBUG) {
|
|
188
|
+
console.error(chalk.gray('\nFull Stack Trace:'));
|
|
189
|
+
console.error(chalk.gray(error.stack));
|
|
190
|
+
} else {
|
|
191
|
+
console.error(chalk.gray('Run with DEBUG=1 for full stack trace'));
|
|
192
|
+
}
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnose Command - Diagnose and report issues with an existing service
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { ServiceOrchestrator } from '../../src/service-management/ServiceOrchestrator.js';
|
|
7
|
+
export function registerDiagnoseCommand(program) {
|
|
8
|
+
program.command('diagnose [service-path]').description('Diagnose and report issues with an existing service').option('--deep-scan', 'Perform deep analysis including dependencies and deployment readiness').option('--export-report <file>', 'Export diagnostic report to file').action(async (servicePath, options) => {
|
|
9
|
+
try {
|
|
10
|
+
const orchestrator = new ServiceOrchestrator();
|
|
11
|
+
|
|
12
|
+
// Auto-detect service path if not provided
|
|
13
|
+
if (!servicePath) {
|
|
14
|
+
servicePath = await orchestrator.detectServicePath();
|
|
15
|
+
if (!servicePath) {
|
|
16
|
+
console.error(chalk.red('No service path provided and could not auto-detect service directory'));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
console.log(chalk.cyan('🔍 Diagnosing service...'));
|
|
21
|
+
const diagnosis = await orchestrator.diagnoseService(servicePath, options);
|
|
22
|
+
|
|
23
|
+
// Display results
|
|
24
|
+
console.log(chalk.cyan('\n📋 Diagnostic Report'));
|
|
25
|
+
console.log(chalk.white(`Service: ${diagnosis.serviceName || 'Unknown'}`));
|
|
26
|
+
console.log(chalk.white(`Path: ${servicePath}`));
|
|
27
|
+
if (diagnosis.errors.length > 0) {
|
|
28
|
+
console.log(chalk.red('\n❌ Critical Errors:'));
|
|
29
|
+
diagnosis.errors.forEach(error => {
|
|
30
|
+
console.log(chalk.red(` • ${error.message}`));
|
|
31
|
+
if (error.location) {
|
|
32
|
+
console.log(chalk.gray(` Location: ${error.location}`));
|
|
33
|
+
}
|
|
34
|
+
if (error.suggestion) {
|
|
35
|
+
console.log(chalk.cyan(` 💡 ${error.suggestion}`));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (diagnosis.warnings.length > 0) {
|
|
40
|
+
console.log(chalk.yellow('\n⚠️ Warnings:'));
|
|
41
|
+
diagnosis.warnings.forEach(warning => {
|
|
42
|
+
console.log(chalk.yellow(` • ${warning.message}`));
|
|
43
|
+
if (warning.suggestion) {
|
|
44
|
+
console.log(chalk.cyan(` 💡 ${warning.suggestion}`));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (diagnosis.recommendations.length > 0) {
|
|
49
|
+
console.log(chalk.cyan('\n💡 Recommendations:'));
|
|
50
|
+
diagnosis.recommendations.forEach(rec => {
|
|
51
|
+
console.log(chalk.white(` • ${rec}`));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Export report if requested
|
|
56
|
+
if (options.exportReport) {
|
|
57
|
+
await orchestrator.exportDiagnosticReport(diagnosis, options.exportReport);
|
|
58
|
+
console.log(chalk.green(`\n📄 Report exported to: ${options.exportReport}`));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Exit with error code if critical issues found
|
|
62
|
+
if (diagnosis.errors.length > 0) {
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(chalk.red(`Diagnosis failed: ${error.message}`));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Helpers - Shared utilities for all commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { existsSync, readFileSync } from 'fs';
|
|
7
|
+
import { join, resolve } from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load JSON configuration file
|
|
11
|
+
*/
|
|
12
|
+
export function loadJsonConfig(configPath) {
|
|
13
|
+
try {
|
|
14
|
+
const fullPath = resolve(configPath);
|
|
15
|
+
if (!existsSync(fullPath)) {
|
|
16
|
+
throw new Error(`Configuration file not found: ${fullPath}`);
|
|
17
|
+
}
|
|
18
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
19
|
+
const config = JSON.parse(content);
|
|
20
|
+
|
|
21
|
+
// Validate required fields
|
|
22
|
+
const required = ['customer', 'environment', 'domainName', 'cloudflareToken'];
|
|
23
|
+
const missing = required.filter(field => !config[field]);
|
|
24
|
+
if (missing.length > 0) {
|
|
25
|
+
throw new Error(`Missing required configuration fields: ${missing.join(', ')}`);
|
|
26
|
+
}
|
|
27
|
+
console.log(chalk.green(`✅ Loaded configuration from: ${fullPath}`));
|
|
28
|
+
return config;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error instanceof SyntaxError) {
|
|
31
|
+
throw new Error(`Invalid JSON in configuration file: ${error.message}`);
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Show progress indicator for deployment steps
|
|
39
|
+
*/
|
|
40
|
+
export function showProgress(message, duration = 2000) {
|
|
41
|
+
return new Promise(resolve => {
|
|
42
|
+
process.stdout.write(chalk.cyan(`⏳ ${message}...`));
|
|
43
|
+
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
44
|
+
let i = 0;
|
|
45
|
+
const interval = setInterval(() => {
|
|
46
|
+
process.stdout.write(`\r${chalk.cyan(spinner[i])} ${message}...`);
|
|
47
|
+
i = (i + 1) % spinner.length;
|
|
48
|
+
}, 100);
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
clearInterval(interval);
|
|
51
|
+
process.stdout.write(`\r${chalk.green('✅')} ${message}... Done!\n`);
|
|
52
|
+
resolve();
|
|
53
|
+
}, duration);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Early validation function to check prerequisites before deployment
|
|
59
|
+
*/
|
|
60
|
+
export async function validateDeploymentPrerequisites(coreInputs, options) {
|
|
61
|
+
const issues = [];
|
|
62
|
+
console.log(chalk.cyan('\n🔍 Pre-deployment Validation'));
|
|
63
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
64
|
+
|
|
65
|
+
// Check required fields
|
|
66
|
+
if (!coreInputs.customer) {
|
|
67
|
+
issues.push('Customer name is required');
|
|
68
|
+
}
|
|
69
|
+
if (!coreInputs.environment) {
|
|
70
|
+
issues.push('Environment is required');
|
|
71
|
+
}
|
|
72
|
+
if (!coreInputs.domainName) {
|
|
73
|
+
issues.push('Domain name is required');
|
|
74
|
+
}
|
|
75
|
+
if (!coreInputs.cloudflareToken) {
|
|
76
|
+
issues.push('Cloudflare API token is required');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check Cloudflare token format (basic validation)
|
|
80
|
+
if (coreInputs.cloudflareToken && !coreInputs.cloudflareToken.startsWith('CLOUDFLARE_API_TOKEN=')) {
|
|
81
|
+
if (coreInputs.cloudflareToken.length < 40) {
|
|
82
|
+
issues.push('Cloudflare API token appears to be invalid (too short)');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check if service path exists
|
|
87
|
+
if (options.servicePath && options.servicePath !== '.') {
|
|
88
|
+
if (!existsSync(options.servicePath)) {
|
|
89
|
+
issues.push(`Service path does not exist: ${options.servicePath}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check for wrangler.toml if not dry run
|
|
94
|
+
if (!options.dryRun) {
|
|
95
|
+
const wranglerPath = join(options.servicePath || '.', 'wrangler.toml');
|
|
96
|
+
if (!existsSync(wranglerPath)) {
|
|
97
|
+
issues.push('wrangler.toml not found in service directory');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Report issues
|
|
102
|
+
if (issues.length > 0) {
|
|
103
|
+
console.log(chalk.red('\n❌ Validation Failed:'));
|
|
104
|
+
issues.forEach(issue => {
|
|
105
|
+
console.log(chalk.red(` • ${issue}`));
|
|
106
|
+
});
|
|
107
|
+
console.log(chalk.gray('\n─'.repeat(40)));
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
console.log(chalk.green('✅ All prerequisites validated'));
|
|
111
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Redact sensitive information from logs
|
|
117
|
+
*/
|
|
118
|
+
export function redactSensitiveInfo(text) {
|
|
119
|
+
if (typeof text !== 'string') return text;
|
|
120
|
+
|
|
121
|
+
// Patterns to redact
|
|
122
|
+
const patterns = [
|
|
123
|
+
// Cloudflare API tokens
|
|
124
|
+
[/(CLOUDFLARE_API_TOKEN=?)(\w{20,})/gi, '$1[REDACTED]'],
|
|
125
|
+
// Generic API tokens/keys
|
|
126
|
+
[/(api[_-]?token|api[_-]?key|auth[_-]?token)["']?[:=]\s*["']?([a-zA-Z0-9_-]{20,})["']?/gi, '$1: [REDACTED]'],
|
|
127
|
+
// Passwords
|
|
128
|
+
[/(password|passwd|pwd)["']?[:=]\s*["']?([^"'\s]{3,})["']?/gi, '$1: [REDACTED]'],
|
|
129
|
+
// Secrets
|
|
130
|
+
[/(secret|key)["']?[:=]\s*["']?([a-zA-Z0-9_-]{10,})["']?/gi, '$1: [REDACTED]'],
|
|
131
|
+
// Account IDs (partial redaction)
|
|
132
|
+
[/(account[_-]?id|zone[_-]?id)["']?[:=]\s*["']?([a-zA-Z0-9]{8})([a-zA-Z0-9]{24,})["']?/gi, '$1: $2[REDACTED]']];
|
|
133
|
+
let redacted = text;
|
|
134
|
+
patterns.forEach(([pattern, replacement]) => {
|
|
135
|
+
redacted = redacted.replace(pattern, replacement);
|
|
136
|
+
});
|
|
137
|
+
return redacted;
|
|
138
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Command - Update an existing service configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { ServiceOrchestrator } from '../../src/service-management/ServiceOrchestrator.js';
|
|
7
|
+
export function registerUpdateCommand(program) {
|
|
8
|
+
program.command('update [service-path]').description('Update an existing service configuration').option('-i, --interactive', 'Run in interactive mode to select what to update').option('--domain-name <domain>', 'Update domain name').option('--cloudflare-token <token>', 'Update Cloudflare API token').option('--cloudflare-account-id <id>', 'Update Cloudflare account ID').option('--cloudflare-zone-id <id>', 'Update Cloudflare zone ID').option('--environment <env>', 'Update target environment: development, staging, production').option('--add-feature <feature>', 'Add a feature flag').option('--remove-feature <feature>', 'Remove a feature flag').option('--regenerate-configs', 'Regenerate all configuration files').option('--fix-errors', 'Attempt to fix common configuration errors').action(async (servicePath, options) => {
|
|
9
|
+
try {
|
|
10
|
+
const orchestrator = new ServiceOrchestrator();
|
|
11
|
+
|
|
12
|
+
// Auto-detect service path if not provided
|
|
13
|
+
if (!servicePath) {
|
|
14
|
+
servicePath = await orchestrator.detectServicePath();
|
|
15
|
+
if (!servicePath) {
|
|
16
|
+
console.error(chalk.red('No service path provided and could not auto-detect service directory'));
|
|
17
|
+
console.log(chalk.white('Please run this command from within a service directory or specify the path'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
console.log(chalk.cyan(`Auto-detected service at: ${servicePath}`));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Validate it's a service directory
|
|
24
|
+
const isValid = await orchestrator.validateService(servicePath);
|
|
25
|
+
if (!isValid.valid) {
|
|
26
|
+
console.log(chalk.yellow('⚠️ Service has configuration issues. Use --fix-errors to attempt automatic fixes.'));
|
|
27
|
+
if (!options.fixErrors) {
|
|
28
|
+
console.log(chalk.white('Issues found:'));
|
|
29
|
+
isValid.issues.forEach(issue => {
|
|
30
|
+
console.log(chalk.yellow(` • ${issue}`));
|
|
31
|
+
});
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (options.interactive) {
|
|
36
|
+
await orchestrator.runInteractiveUpdate(servicePath);
|
|
37
|
+
} else {
|
|
38
|
+
await orchestrator.runNonInteractiveUpdate(servicePath, options);
|
|
39
|
+
}
|
|
40
|
+
console.log(chalk.green('\n✓ Service update completed successfully!'));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(chalk.red(`\n✗ Service update failed: ${error.message}`));
|
|
43
|
+
if (error.details) {
|
|
44
|
+
console.error(chalk.yellow(`Details: ${error.details}`));
|
|
45
|
+
}
|
|
46
|
+
if (error.recovery) {
|
|
47
|
+
console.log(chalk.cyan('\n💡 Recovery suggestions:'));
|
|
48
|
+
error.recovery.forEach(suggestion => {
|
|
49
|
+
console.log(chalk.white(` • ${suggestion}`));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|