@tamyla/clodo-framework 3.1.14 ā 3.1.15
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 +7 -0
- package/dist/bin/commands/assess.js +46 -31
- package/dist/bin/commands/create.js +45 -24
- package/dist/bin/commands/deploy.js +239 -83
- package/dist/bin/commands/diagnose.js +45 -32
- package/dist/bin/commands/update.js +39 -19
- package/dist/bin/commands/validate.js +33 -8
- package/dist/bin/shared/deployment/credential-collector.js +4 -6
- package/dist/bin/shared/routing/domain-router.js +596 -0
- package/dist/bin/shared/utils/ErrorHandler.js +122 -2
- package/dist/bin/shared/utils/cli-options.js +136 -0
- package/dist/bin/shared/utils/config-loader.js +275 -0
- package/dist/bin/shared/utils/output-formatter.js +273 -0
- package/dist/bin/shared/utils/progress-manager.js +335 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [3.1.15](https://github.com/tamylaa/clodo-framework/compare/v3.1.14...v3.1.15) (2025-10-28)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Complete comprehensive clodo-framework refactoring (Tasks 3.1-3.4) ([0949b8b](https://github.com/tamylaa/clodo-framework/commit/0949b8be5f4f6522c0e4e4050e858ce82be5d034))
|
|
7
|
+
|
|
1
8
|
## [3.1.14](https://github.com/tamylaa/clodo-framework/compare/v3.1.13...v3.1.14) (2025-10-27)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -4,16 +4,43 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
+
import { StandardOptions } from '../shared/utils/cli-options.js';
|
|
8
|
+
import { ConfigLoader } from '../shared/utils/config-loader.js';
|
|
7
9
|
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')
|
|
10
|
+
const command = 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');
|
|
11
|
+
|
|
12
|
+
// Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
|
|
13
|
+
StandardOptions.define(command).action(async (servicePath, options) => {
|
|
9
14
|
try {
|
|
15
|
+
const output = new (await import('../shared/utils/output-formatter.js')).OutputFormatter(options);
|
|
16
|
+
const configLoader = new ConfigLoader({
|
|
17
|
+
verbose: options.verbose,
|
|
18
|
+
quiet: options.quiet,
|
|
19
|
+
json: options.json
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Load config from file if specified
|
|
23
|
+
let configFileData = {};
|
|
24
|
+
if (options.configFile) {
|
|
25
|
+
configFileData = configLoader.loadSafe(options.configFile, {});
|
|
26
|
+
if (options.verbose && !options.quiet) {
|
|
27
|
+
output.info(`Loaded configuration from: ${options.configFile}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Substitute environment variables
|
|
32
|
+
configFileData = configLoader.substituteEnvironmentVariables(configFileData);
|
|
33
|
+
|
|
34
|
+
// Merge config file defaults with CLI options (CLI takes precedence)
|
|
35
|
+
const mergedOptions = configLoader.merge(configFileData, options);
|
|
36
|
+
|
|
10
37
|
// Try to load professional orchestration package
|
|
11
38
|
let orchestrationModule;
|
|
12
39
|
try {
|
|
13
40
|
orchestrationModule = await import('@tamyla/clodo-orchestration');
|
|
14
41
|
} catch (err) {
|
|
15
|
-
|
|
16
|
-
|
|
42
|
+
output.error('ā clodo-orchestration package not found');
|
|
43
|
+
output.info('š” Install with: npm install @tamyla/clodo-orchestration');
|
|
17
44
|
process.exit(1);
|
|
18
45
|
}
|
|
19
46
|
const {
|
|
@@ -22,53 +49,41 @@ export function registerAssessCommand(program) {
|
|
|
22
49
|
runAssessmentWorkflow
|
|
23
50
|
} = orchestrationModule;
|
|
24
51
|
const targetPath = servicePath || process.cwd();
|
|
25
|
-
|
|
26
|
-
|
|
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)));
|
|
52
|
+
output.section('Professional Capability Assessment');
|
|
53
|
+
output.list([`Service Path: ${targetPath}`, mergedOptions.domain ? `Domain: ${mergedOptions.domain}` : null, mergedOptions.serviceType ? `Service Type: ${mergedOptions.serviceType}` : null].filter(Boolean));
|
|
35
54
|
|
|
36
55
|
// Use the assessment workflow
|
|
37
56
|
const assessment = await runAssessmentWorkflow({
|
|
38
57
|
servicePath: targetPath,
|
|
39
|
-
domain:
|
|
40
|
-
serviceType:
|
|
41
|
-
token:
|
|
58
|
+
domain: mergedOptions.domain,
|
|
59
|
+
serviceType: mergedOptions.serviceType,
|
|
60
|
+
token: mergedOptions.token || process.env.CLOUDFLARE_API_TOKEN
|
|
42
61
|
});
|
|
43
62
|
|
|
44
63
|
// Display results
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.log(chalk.white(`Service Type: ${assessment.mergedInputs?.serviceType || assessment.serviceType || 'Not determined'}`));
|
|
48
|
-
console.log(chalk.white(`Confidence: ${assessment.confidence}%`));
|
|
64
|
+
output.section('ā
Assessment Results');
|
|
65
|
+
output.list([`Service Type: ${assessment.mergedInputs?.serviceType || assessment.serviceType || 'Not determined'}`, `Confidence: ${assessment.confidence}%`]);
|
|
49
66
|
if (assessment.gapAnalysis?.missing) {
|
|
50
|
-
console.log(chalk.white(`Missing Capabilities: ${assessment.gapAnalysis.missing.length}`));
|
|
51
67
|
if (assessment.gapAnalysis.missing.length > 0) {
|
|
52
|
-
|
|
53
|
-
assessment.gapAnalysis.missing.
|
|
54
|
-
|
|
55
|
-
});
|
|
68
|
+
output.warning('ā ļø Missing Capabilities:');
|
|
69
|
+
const missingItems = assessment.gapAnalysis.missing.map(gap => `${gap.capability}: ${gap.reason || 'Not available'}`);
|
|
70
|
+
output.list(missingItems);
|
|
56
71
|
}
|
|
57
72
|
}
|
|
58
|
-
console.log(chalk.gray('ā'.repeat(60)));
|
|
59
73
|
|
|
60
74
|
// Export results if requested
|
|
61
|
-
if (
|
|
75
|
+
if (mergedOptions.export) {
|
|
62
76
|
const {
|
|
63
77
|
writeFileSync
|
|
64
78
|
} = await import('fs');
|
|
65
|
-
writeFileSync(
|
|
66
|
-
|
|
79
|
+
writeFileSync(mergedOptions.export, JSON.stringify(assessment, null, 2));
|
|
80
|
+
output.success(`š Results exported to: ${mergedOptions.export}`);
|
|
67
81
|
}
|
|
68
82
|
} catch (error) {
|
|
69
|
-
|
|
83
|
+
const output = new (await import('../shared/utils/output-formatter.js')).OutputFormatter(options || {});
|
|
84
|
+
output.error(`Assessment failed: ${error.message}`);
|
|
70
85
|
if (process.env.DEBUG) {
|
|
71
|
-
|
|
86
|
+
output.debug(error.stack);
|
|
72
87
|
}
|
|
73
88
|
process.exit(1);
|
|
74
89
|
}
|
|
@@ -7,48 +7,69 @@
|
|
|
7
7
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { ServiceOrchestrator } from "../../service-management/ServiceOrchestrator.js";
|
|
10
|
+
import { StandardOptions } from '../shared/utils/cli-options.js';
|
|
11
|
+
import { ConfigLoader } from '../shared/utils/config-loader.js';
|
|
10
12
|
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').
|
|
13
|
+
const command = 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').option('--force', 'Skip confirmation prompts').option('--validate', 'Validate service after creation');
|
|
14
|
+
|
|
15
|
+
// Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
|
|
16
|
+
StandardOptions.define(command).action(async options => {
|
|
12
17
|
try {
|
|
18
|
+
const output = new (await import('../shared/utils/output-formatter.js')).OutputFormatter(options);
|
|
19
|
+
const configLoader = new ConfigLoader({
|
|
20
|
+
verbose: options.verbose,
|
|
21
|
+
quiet: options.quiet,
|
|
22
|
+
json: options.json
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Load config from file if specified
|
|
26
|
+
let configFileData = {};
|
|
27
|
+
if (options.configFile) {
|
|
28
|
+
configFileData = configLoader.loadSafe(options.configFile, {});
|
|
29
|
+
if (options.verbose && !options.quiet) {
|
|
30
|
+
output.info(`Loaded configuration from: ${options.configFile}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Merge config file defaults with CLI options (CLI takes precedence)
|
|
35
|
+
const mergedOptions = configLoader.merge(configFileData, options);
|
|
13
36
|
const orchestrator = new ServiceOrchestrator({
|
|
14
|
-
interactive: !
|
|
15
|
-
outputPath:
|
|
16
|
-
templatePath:
|
|
37
|
+
interactive: !mergedOptions.nonInteractive,
|
|
38
|
+
outputPath: mergedOptions.outputPath,
|
|
39
|
+
templatePath: mergedOptions.templatePath
|
|
17
40
|
});
|
|
18
|
-
if (
|
|
41
|
+
if (mergedOptions.nonInteractive) {
|
|
19
42
|
// Validate required parameters for non-interactive mode
|
|
20
43
|
const required = ['serviceName', 'domainName', 'cloudflareToken', 'cloudflareAccountId', 'cloudflareZoneId'];
|
|
21
|
-
const missing = required.filter(key => !
|
|
44
|
+
const missing = required.filter(key => !mergedOptions[key]);
|
|
22
45
|
if (missing.length > 0) {
|
|
23
|
-
|
|
24
|
-
|
|
46
|
+
output.error(`Missing required parameters: ${missing.join(', ')}`);
|
|
47
|
+
output.info('Use --help for parameter details');
|
|
25
48
|
process.exit(1);
|
|
26
49
|
}
|
|
27
50
|
|
|
28
|
-
// Convert
|
|
51
|
+
// Convert merged options to core inputs
|
|
29
52
|
const coreInputs = {
|
|
30
|
-
serviceName:
|
|
31
|
-
serviceType:
|
|
32
|
-
domainName:
|
|
33
|
-
cloudflareToken:
|
|
34
|
-
cloudflareAccountId:
|
|
35
|
-
cloudflareZoneId:
|
|
36
|
-
environment:
|
|
53
|
+
serviceName: mergedOptions.serviceName,
|
|
54
|
+
serviceType: mergedOptions.serviceType,
|
|
55
|
+
domainName: mergedOptions.domainName,
|
|
56
|
+
cloudflareToken: mergedOptions.cloudflareToken,
|
|
57
|
+
cloudflareAccountId: mergedOptions.cloudflareAccountId,
|
|
58
|
+
cloudflareZoneId: mergedOptions.cloudflareZoneId,
|
|
59
|
+
environment: mergedOptions.environment
|
|
37
60
|
};
|
|
38
61
|
await orchestrator.runNonInteractive(coreInputs);
|
|
39
62
|
} else {
|
|
40
63
|
await orchestrator.runInteractive();
|
|
41
64
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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'));
|
|
65
|
+
output.success('Service creation completed successfully!');
|
|
66
|
+
output.section('Next steps');
|
|
67
|
+
output.list(['cd into your new service directory', 'Run npm install', 'Configure additional settings in src/config/domains.js', 'Run npm run deploy to deploy to Cloudflare']);
|
|
48
68
|
} catch (error) {
|
|
49
|
-
|
|
69
|
+
const output = new (await import('../shared/utils/output-formatter.js')).OutputFormatter(options || {});
|
|
70
|
+
output.error(`Service creation failed: ${error.message}`);
|
|
50
71
|
if (error.details) {
|
|
51
|
-
|
|
72
|
+
output.warning(`Details: ${error.details}`);
|
|
52
73
|
}
|
|
53
74
|
process.exit(1);
|
|
54
75
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deploy Command - Smart minimal input deployment with service detection
|
|
3
3
|
*
|
|
4
|
-
* Input Strategy: SMART MINIMAL
|
|
4
|
+
* Input Strategy: SMART MINIMAL WITH DOMAIN ROUTING
|
|
5
5
|
* - Detects Clodo services OR legacy services (wrangler.toml)
|
|
6
6
|
* - Supports multiple manifest locations
|
|
7
|
+
* - Uses DomainRouter for intelligent domain selection
|
|
7
8
|
* - Gathers credentials smartly: env vars ā flags ā interactive collection with auto-fetch
|
|
8
9
|
* - Validates token and fetches account ID & zone ID from Cloudflare
|
|
9
|
-
* - Integrates with
|
|
10
|
+
* - REFACTORED (Task 3.2): Integrates with MultiDomainOrchestrator for full deployment orchestration
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import chalk from 'chalk';
|
|
@@ -14,13 +15,43 @@ import { resolve } from 'path';
|
|
|
14
15
|
import { ManifestLoader } from '../shared/config/manifest-loader.js';
|
|
15
16
|
import { CloudflareServiceValidator } from '../shared/config/cloudflare-service-validator.js';
|
|
16
17
|
import { DeploymentCredentialCollector } from '../shared/deployment/credential-collector.js';
|
|
18
|
+
import { StandardOptions } from '../shared/utils/cli-options.js';
|
|
19
|
+
import { ConfigLoader } from '../shared/utils/config-loader.js';
|
|
20
|
+
import { DomainRouter } from '../shared/routing/domain-router.js';
|
|
21
|
+
import { MultiDomainOrchestrator } from "../../orchestration/multi-domain-orchestrator.js";
|
|
17
22
|
export function registerDeployCommand(program) {
|
|
18
|
-
program.command('deploy').description('Deploy a Clodo service with smart credential handling
|
|
23
|
+
const command = program.command('deploy').description('Deploy a Clodo service with smart credential handling and domain selection')
|
|
24
|
+
// Cloudflare-specific options
|
|
25
|
+
.option('--token <token>', 'Cloudflare API token').option('--account-id <id>', 'Cloudflare account ID').option('--zone-id <id>', 'Cloudflare zone ID').option('--domain <domain>', 'Specific domain to deploy to (otherwise prompted if multiple exist)').option('--environment <env>', 'Target environment (development, staging, production)', 'production').option('--all-domains', 'Deploy to all configured domains (ignores --domain flag)').option('--dry-run', 'Simulate deployment without making changes').option('--service-path <path>', 'Path to service directory', '.');
|
|
26
|
+
|
|
27
|
+
// Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
|
|
28
|
+
StandardOptions.define(command).action(async options => {
|
|
19
29
|
try {
|
|
20
|
-
|
|
30
|
+
const output = new (await import('../shared/utils/output-formatter.js')).OutputFormatter(options);
|
|
31
|
+
const configLoader = new ConfigLoader({
|
|
32
|
+
verbose: options.verbose,
|
|
33
|
+
quiet: options.quiet,
|
|
34
|
+
json: options.json
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Load config from file if specified
|
|
38
|
+
let configFileData = {};
|
|
39
|
+
if (options.configFile) {
|
|
40
|
+
configFileData = configLoader.loadSafe(options.configFile, {});
|
|
41
|
+
if (options.verbose && !options.quiet) {
|
|
42
|
+
output.info(`Loaded configuration from: ${options.configFile}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Substitute environment variables in config
|
|
47
|
+
configFileData = configLoader.substituteEnvironmentVariables(configFileData);
|
|
48
|
+
|
|
49
|
+
// Merge config file defaults with CLI options (CLI takes precedence)
|
|
50
|
+
const mergedOptions = configLoader.merge(configFileData, options);
|
|
51
|
+
output.info('š Clodo Service Deployment');
|
|
21
52
|
|
|
22
53
|
// Step 1: Load and validate service configuration
|
|
23
|
-
const servicePath = resolve(
|
|
54
|
+
const servicePath = resolve(mergedOptions.servicePath || '.');
|
|
24
55
|
const serviceConfig = await ManifestLoader.loadAndValidateCloudflareService(servicePath);
|
|
25
56
|
if (!serviceConfig.manifest) {
|
|
26
57
|
if (serviceConfig.error === 'NOT_A_CLOUDFLARE_SERVICE') {
|
|
@@ -37,7 +68,7 @@ export function registerDeployCommand(program) {
|
|
|
37
68
|
CloudflareServiceValidator.printValidationReport(serviceConfig.validationResult.validation);
|
|
38
69
|
}
|
|
39
70
|
ManifestLoader.printManifestInfo(serviceConfig.manifest);
|
|
40
|
-
|
|
71
|
+
output.info(`Configuration loaded from: ${serviceConfig.foundAt}`);
|
|
41
72
|
|
|
42
73
|
// Step 2: Smart credential gathering with interactive collection
|
|
43
74
|
// Uses DeploymentCredentialCollector which:
|
|
@@ -47,126 +78,246 @@ export function registerDeployCommand(program) {
|
|
|
47
78
|
// - Caches credentials for future use
|
|
48
79
|
const credentialCollector = new DeploymentCredentialCollector({
|
|
49
80
|
servicePath: servicePath,
|
|
50
|
-
quiet:
|
|
81
|
+
quiet: mergedOptions.quiet
|
|
51
82
|
});
|
|
52
83
|
let credentials;
|
|
53
84
|
try {
|
|
54
85
|
credentials = await credentialCollector.collectCredentials({
|
|
55
|
-
token:
|
|
56
|
-
accountId:
|
|
57
|
-
zoneId:
|
|
86
|
+
token: mergedOptions.token,
|
|
87
|
+
accountId: mergedOptions.accountId,
|
|
88
|
+
zoneId: mergedOptions.zoneId
|
|
58
89
|
});
|
|
59
90
|
} finally {
|
|
60
91
|
credentialCollector.cleanup();
|
|
61
92
|
}
|
|
62
93
|
|
|
63
|
-
// Step 3:
|
|
94
|
+
// Step 3: Initialize DomainRouter for intelligent domain selection
|
|
95
|
+
// REFACTORED (Task 3.2): Use DomainRouter for domain detection and selection
|
|
96
|
+
console.log(chalk.cyan('\nšŗļø Detecting available domains...\n'));
|
|
97
|
+
const router = new DomainRouter({
|
|
98
|
+
environment: mergedOptions.environment || 'production',
|
|
99
|
+
verbose: options.verbose,
|
|
100
|
+
configPath: options.configPath,
|
|
101
|
+
disableOrchestrator: false,
|
|
102
|
+
// We'll use the orchestrator for deployment
|
|
103
|
+
orchestratorOptions: {
|
|
104
|
+
dryRun: options.dryRun || false,
|
|
105
|
+
cloudflareToken: credentials.token,
|
|
106
|
+
cloudflareAccountId: credentials.accountId
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Detect domains from manifest
|
|
111
|
+
let detectedDomains = [];
|
|
64
112
|
const manifest = serviceConfig.manifest;
|
|
65
113
|
const config = manifest.deployment || manifest.configuration || {};
|
|
114
|
+
const domainsConfig = config.domains || [];
|
|
115
|
+
if (Array.isArray(domainsConfig) && domainsConfig.length > 0) {
|
|
116
|
+
// Handle both array of strings and array of objects
|
|
117
|
+
detectedDomains = domainsConfig.map(d => {
|
|
118
|
+
if (typeof d === 'string') return d;
|
|
119
|
+
if (typeof d === 'object' && d.name) return d.name;
|
|
120
|
+
return null;
|
|
121
|
+
}).filter(d => d !== null);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// If no domains in config, try to detect from environment
|
|
125
|
+
if (detectedDomains.length === 0 && manifest._source === 'cloudflare-service-detected') {
|
|
126
|
+
// For detected CF services, use default
|
|
127
|
+
detectedDomains = ['workers.cloudflare.com'];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Domain selection: check CLI flag first, then prompt user
|
|
131
|
+
let selectedDomain = mergedOptions.domain;
|
|
132
|
+
if (!selectedDomain && detectedDomains.length > 0) {
|
|
133
|
+
if (detectedDomains.length === 1) {
|
|
134
|
+
// Only one domain, use it directly
|
|
135
|
+
selectedDomain = detectedDomains[0];
|
|
136
|
+
if (!options.quiet) {
|
|
137
|
+
console.log(chalk.green(`ā Selected domain: ${selectedDomain}`));
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// Multiple domains available - let user choose
|
|
141
|
+
console.log(chalk.cyan('š Available domains:'));
|
|
142
|
+
detectedDomains.forEach((d, i) => {
|
|
143
|
+
console.log(chalk.white(` ${i + 1}) ${d}`));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// If running interactively, prompt user
|
|
147
|
+
if (process.stdin.isTTY) {
|
|
148
|
+
const {
|
|
149
|
+
createPromptModule
|
|
150
|
+
} = await import('inquirer');
|
|
151
|
+
const prompt = createPromptModule();
|
|
152
|
+
const response = await prompt([{
|
|
153
|
+
type: 'list',
|
|
154
|
+
name: 'selectedDomain',
|
|
155
|
+
message: 'Select domain to deploy to:',
|
|
156
|
+
choices: detectedDomains,
|
|
157
|
+
default: detectedDomains[0]
|
|
158
|
+
}]);
|
|
159
|
+
selectedDomain = response.selectedDomain;
|
|
160
|
+
} else {
|
|
161
|
+
// Non-interactive mode: use first domain
|
|
162
|
+
selectedDomain = detectedDomains[0];
|
|
163
|
+
console.log(chalk.yellow(`ā ļø Non-interactive mode: using first domain: ${selectedDomain}`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!selectedDomain) {
|
|
168
|
+
if (!options.quiet) {
|
|
169
|
+
console.error(chalk.yellow('ā ļø No domain configured for deployment'));
|
|
170
|
+
console.error(chalk.gray('For Clodo services: add deployment.domains in clodo-service-manifest.json'));
|
|
171
|
+
console.error(chalk.gray('For detected CF services: define routes in wrangler.toml'));
|
|
172
|
+
}
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Step 4: Validate domain configuration
|
|
177
|
+
console.log(chalk.cyan('\nš Validating domain configuration...\n'));
|
|
178
|
+
const validation = router.validateConfiguration({
|
|
179
|
+
domains: [selectedDomain],
|
|
180
|
+
environment: mergedOptions.environment || 'production'
|
|
181
|
+
});
|
|
182
|
+
if (!validation.valid) {
|
|
183
|
+
console.error(chalk.red('ā Configuration validation failed:'));
|
|
184
|
+
validation.errors.forEach(err => {
|
|
185
|
+
console.error(chalk.yellow(` ⢠${err}`));
|
|
186
|
+
});
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
if (validation.warnings && validation.warnings.length > 0) {
|
|
190
|
+
console.log(chalk.yellow('ā ļø Configuration warnings:'));
|
|
191
|
+
validation.warnings.forEach(warn => {
|
|
192
|
+
console.log(chalk.gray(` ⢠${warn}`));
|
|
193
|
+
});
|
|
194
|
+
}
|
|
66
195
|
|
|
67
196
|
// Extract service metadata
|
|
68
197
|
const serviceName = manifest.serviceName || 'unknown-service';
|
|
69
198
|
const serviceType = manifest.serviceType || 'generic';
|
|
70
199
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
console.log(chalk.gray('Note: Using Cloudflare Workers default domain (add routes in wrangler.toml for custom domains)'));
|
|
83
|
-
}
|
|
84
|
-
if (!domain && !options.quiet) {
|
|
85
|
-
console.error(chalk.yellow('ā ļø No domain configured for deployment'));
|
|
86
|
-
console.error(chalk.gray('For Clodo services: add deployment.domains in clodo-service-manifest.json'));
|
|
87
|
-
console.error(chalk.gray('For detected CF services: define routes in wrangler.toml'));
|
|
88
|
-
}
|
|
89
|
-
console.log(chalk.cyan('š Deployment Plan:'));
|
|
90
|
-
console.log(chalk.gray('ā'.repeat(50)));
|
|
91
|
-
console.log(chalk.white(`Service: ${serviceName}`));
|
|
92
|
-
console.log(chalk.white(`Type: ${serviceType}`));
|
|
93
|
-
if (domain) {
|
|
94
|
-
console.log(chalk.white(`Domain: ${domain}`));
|
|
95
|
-
}
|
|
96
|
-
console.log(chalk.white(`Account: ${credentials.accountId.substring(0, 8)}...`));
|
|
97
|
-
console.log(chalk.white(`Zone: ${credentials.zoneId.substring(0, 8)}...`));
|
|
98
|
-
console.log(chalk.gray('ā'.repeat(50)));
|
|
200
|
+
// Display deployment plan
|
|
201
|
+
console.log(chalk.cyan('\nš Deployment Plan:'));
|
|
202
|
+
console.log(chalk.gray('ā'.repeat(60)));
|
|
203
|
+
console.log(chalk.white(`Service: ${serviceName}`));
|
|
204
|
+
console.log(chalk.white(`Type: ${serviceType}`));
|
|
205
|
+
console.log(chalk.white(`Domain: ${selectedDomain}`));
|
|
206
|
+
console.log(chalk.white(`Environment: ${mergedOptions.environment || 'production'}`));
|
|
207
|
+
console.log(chalk.white(`Account: ${credentials.accountId.substring(0, 8)}...`));
|
|
208
|
+
console.log(chalk.white(`Zone: ${credentials.zoneId.substring(0, 8)}...`));
|
|
209
|
+
console.log(chalk.white(`Deployment Mode: ${options.dryRun ? 'DRY RUN' : 'LIVE'}`));
|
|
210
|
+
console.log(chalk.gray('ā'.repeat(60)));
|
|
99
211
|
if (options.dryRun) {
|
|
100
212
|
console.log(chalk.yellow('\nš DRY RUN MODE - No changes will be made\n'));
|
|
101
213
|
}
|
|
102
214
|
|
|
103
|
-
// Step
|
|
104
|
-
|
|
105
|
-
|
|
215
|
+
// Step 5: Initialize MultiDomainOrchestrator for deployment
|
|
216
|
+
// REFACTORED (Task 3.2): Direct orchestrator integration instead of external deployer
|
|
217
|
+
console.log(chalk.cyan('\nāļø Initializing orchestration system...\n'));
|
|
218
|
+
const orchestrator = new MultiDomainOrchestrator({
|
|
219
|
+
domains: [selectedDomain],
|
|
220
|
+
environment: mergedOptions.environment || 'production',
|
|
221
|
+
dryRun: options.dryRun || false,
|
|
222
|
+
skipTests: false,
|
|
223
|
+
parallelDeployments: 1,
|
|
224
|
+
// Single domain in this flow
|
|
225
|
+
servicePath: servicePath,
|
|
226
|
+
cloudflareToken: credentials.token,
|
|
227
|
+
cloudflareAccountId: credentials.accountId,
|
|
228
|
+
enablePersistence: !options.dryRun,
|
|
229
|
+
rollbackEnabled: !options.dryRun,
|
|
230
|
+
verbose: options.verbose
|
|
231
|
+
});
|
|
106
232
|
try {
|
|
107
|
-
|
|
108
|
-
ModularEnterpriseDeployer = module.ModularEnterpriseDeployer || module.default;
|
|
233
|
+
await orchestrator.initialize();
|
|
109
234
|
} catch (err) {
|
|
110
|
-
console.error(chalk.red('ā Failed to
|
|
235
|
+
console.error(chalk.red('ā Failed to initialize orchestrator'));
|
|
111
236
|
console.error(chalk.yellow(`Error: ${err.message}`));
|
|
112
237
|
if (process.env.DEBUG) {
|
|
113
238
|
console.error(chalk.gray(err.stack));
|
|
114
239
|
}
|
|
115
240
|
process.exit(1);
|
|
116
241
|
}
|
|
117
|
-
if (!ModularEnterpriseDeployer) {
|
|
118
|
-
console.error(chalk.red('ā ModularEnterpriseDeployer not found in deployment module'));
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
242
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
243
|
+
// Step 6: Execute deployment via orchestrator
|
|
244
|
+
console.log(chalk.cyan('š Starting deployment via orchestrator...\n'));
|
|
245
|
+
let result;
|
|
246
|
+
try {
|
|
247
|
+
result = await orchestrator.deploySingleDomain(selectedDomain, {
|
|
248
|
+
manifest: manifest,
|
|
249
|
+
credentials: credentials,
|
|
250
|
+
dryRun: options.dryRun,
|
|
251
|
+
environment: mergedOptions.environment || 'production'
|
|
252
|
+
});
|
|
253
|
+
} catch (deployError) {
|
|
254
|
+
console.error(chalk.red('\nā Deployment failed during orchestration\n'));
|
|
255
|
+
console.error(chalk.yellow(`Error: ${deployError.message}`));
|
|
134
256
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
257
|
+
// Provide helpful error context
|
|
258
|
+
if (deployError.message.includes('credentials') || deployError.message.includes('auth')) {
|
|
259
|
+
console.error(chalk.yellow('\nš” Credential Issue:'));
|
|
260
|
+
console.error(chalk.white(' Check your API token, account ID, and zone ID'));
|
|
261
|
+
console.error(chalk.white(' Visit: https://dash.cloudflare.com/profile/api-tokens'));
|
|
262
|
+
}
|
|
263
|
+
if (deployError.message.includes('domain') || deployError.message.includes('zone')) {
|
|
264
|
+
console.error(chalk.yellow('\nļæ½ Domain Issue:'));
|
|
265
|
+
console.error(chalk.white(' Verify domain exists in Cloudflare'));
|
|
266
|
+
console.error(chalk.white(' Check API token has zone:read permissions'));
|
|
267
|
+
}
|
|
268
|
+
if (process.env.DEBUG) {
|
|
269
|
+
console.error(chalk.gray('\nFull Stack Trace:'));
|
|
270
|
+
console.error(chalk.gray(deployError.stack));
|
|
271
|
+
}
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
142
274
|
|
|
143
|
-
// Display results
|
|
275
|
+
// Step 7: Display deployment results
|
|
144
276
|
console.log(chalk.green('\nā
Deployment Completed Successfully!\n'));
|
|
145
|
-
console.log(chalk.gray('ā'.repeat(
|
|
277
|
+
console.log(chalk.gray('ā'.repeat(60)));
|
|
278
|
+
|
|
279
|
+
// Display results from orchestrator
|
|
146
280
|
if (result.url) {
|
|
147
|
-
console.log(chalk.white(`š Service URL:
|
|
281
|
+
console.log(chalk.white(`š Service URL: ${chalk.bold(result.url)}`));
|
|
148
282
|
}
|
|
149
|
-
console.log(chalk.white(`š¦ Service:
|
|
150
|
-
console.log(chalk.white(`š§ Type:
|
|
151
|
-
console.log(chalk.white(`š Domain:
|
|
283
|
+
console.log(chalk.white(`š¦ Service: ${serviceName}`));
|
|
284
|
+
console.log(chalk.white(`š§ Type: ${serviceType}`));
|
|
285
|
+
console.log(chalk.white(`š Domain: ${selectedDomain}`));
|
|
286
|
+
console.log(chalk.white(`š Environment: ${mergedOptions.environment || 'production'}`));
|
|
152
287
|
if (result.workerId) {
|
|
153
|
-
console.log(chalk.white(`š¤ Worker ID:
|
|
288
|
+
console.log(chalk.white(`š¤ Worker ID: ${result.workerId}`));
|
|
289
|
+
}
|
|
290
|
+
if (result.deploymentId) {
|
|
291
|
+
console.log(chalk.white(`š Deployment ID: ${result.deploymentId}`));
|
|
154
292
|
}
|
|
155
293
|
if (result.status) {
|
|
156
294
|
const statusColor = result.status.toLowerCase().includes('success') ? chalk.green : chalk.yellow;
|
|
157
|
-
console.log(chalk.white(`š Status:
|
|
295
|
+
console.log(chalk.white(`š Status: ${statusColor(result.status)}`));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Display audit information if available
|
|
299
|
+
if (result.auditLog) {
|
|
300
|
+
console.log(chalk.cyan('\nš Deployment Audit:'));
|
|
301
|
+
console.log(chalk.gray(` Started: ${result.auditLog.startTime}`));
|
|
302
|
+
console.log(chalk.gray(` Completed: ${result.auditLog.endTime}`));
|
|
303
|
+
console.log(chalk.gray(` Duration: ${result.auditLog.duration}ms`));
|
|
158
304
|
}
|
|
159
|
-
console.log(chalk.gray('ā'.repeat(
|
|
305
|
+
console.log(chalk.gray('ā'.repeat(60)));
|
|
160
306
|
|
|
161
|
-
//
|
|
307
|
+
// Display next steps
|
|
162
308
|
if (!options.dryRun) {
|
|
163
309
|
console.log(chalk.cyan('\nš” Next Steps:'));
|
|
164
|
-
console.log(chalk.white(
|
|
165
|
-
console.log(chalk.white(
|
|
166
|
-
console.log(chalk.white(
|
|
310
|
+
console.log(chalk.white(` ⢠Test deployment: curl ${result.url || `https://${selectedDomain}`}`));
|
|
311
|
+
console.log(chalk.white(` ⢠View logs: wrangler tail ${serviceName}`));
|
|
312
|
+
console.log(chalk.white(` ⢠Monitor: https://dash.cloudflare.com`));
|
|
313
|
+
console.log(chalk.white(` ⢠Check audit logs: See deployment ID above\n`));
|
|
314
|
+
} else {
|
|
315
|
+
console.log(chalk.cyan('\nš” Dry Run Complete'));
|
|
316
|
+
console.log(chalk.white(` ⢠Review the plan above`));
|
|
317
|
+
console.log(chalk.white(` ⢠Remove --dry-run to execute deployment\n`));
|
|
167
318
|
}
|
|
168
319
|
if (process.env.DEBUG && result.details) {
|
|
169
|
-
console.log(chalk.gray('
|
|
320
|
+
console.log(chalk.gray('š Full Result:'));
|
|
170
321
|
console.log(chalk.gray(JSON.stringify(result, null, 2)));
|
|
171
322
|
}
|
|
172
323
|
} catch (error) {
|
|
@@ -176,11 +327,16 @@ export function registerDeployCommand(program) {
|
|
|
176
327
|
console.error(chalk.white(' Check your API token, account ID, and zone ID'));
|
|
177
328
|
console.error(chalk.white(' Visit: https://dash.cloudflare.com/profile/api-tokens'));
|
|
178
329
|
}
|
|
179
|
-
if (error.message.includes('domain')) {
|
|
330
|
+
if (error.message.includes('domain') || error.message.includes('zone')) {
|
|
180
331
|
console.error(chalk.yellow('\nš” Domain Issue:'));
|
|
181
332
|
console.error(chalk.white(' Verify domain exists in Cloudflare'));
|
|
182
333
|
console.error(chalk.white(' Check API token has zone:read permissions'));
|
|
183
334
|
}
|
|
335
|
+
if (error.message.includes('orchestration') || error.message.includes('initialization')) {
|
|
336
|
+
console.error(chalk.yellow('\nš” Orchestration Issue:'));
|
|
337
|
+
console.error(chalk.white(' Check MultiDomainOrchestrator configuration'));
|
|
338
|
+
console.error(chalk.white(' Verify all modular components loaded correctly'));
|
|
339
|
+
}
|
|
184
340
|
if (process.env.DEBUG) {
|
|
185
341
|
console.error(chalk.gray('\nFull Stack Trace:'));
|
|
186
342
|
console.error(chalk.gray(error.stack));
|