@tamyla/clodo-framework 3.1.23 → 3.1.24
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 +8 -0
- package/dist/cli/commands/assess.js +26 -17
- package/dist/cli/commands/deploy.js +28 -6
- package/dist/cli/commands/diagnose.js +22 -22
- package/dist/cli/commands/update.js +42 -15
- package/dist/cli/commands/validate.js +28 -14
- package/dist/lib/shared/config/manifest-loader.js +3 -3
- package/dist/lib/shared/utils/config-loader.js +47 -11
- package/dist/lib/shared/utils/service-config-manager.js +227 -0
- package/dist/service-management/ServiceOrchestrator.js +43 -2
- package/dist/service-management/handlers/ValidationHandler.js +22 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [3.1.24](https://github.com/tamylaa/clodo-framework/compare/v3.1.23...v3.1.24) (2025-11-08)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Add comprehensive framework assessment and strategic evolution plan ([bed4108](https://github.com/tamylaa/clodo-framework/commit/bed41085c253e025bd065e507296efe261616652))
|
|
7
|
+
* resolve linting error and enhance CI pipeline ([ece5b5a](https://github.com/tamylaa/clodo-framework/commit/ece5b5aa7232bc990b1d85d3e53ccf7e2d215c98))
|
|
8
|
+
|
|
1
9
|
## [3.1.23](https://github.com/tamylaa/clodo-framework/compare/v3.1.22...v3.1.23) (2025-11-07)
|
|
2
10
|
|
|
3
11
|
|
|
@@ -4,35 +4,46 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { ServiceOrchestrator } from '../service-management/ServiceOrchestrator.js';
|
|
7
9
|
import { StandardOptions } from '../lib/shared/utils/cli-options.js';
|
|
8
|
-
import {
|
|
10
|
+
import { ServiceConfigManager } from '../lib/shared/utils/service-config-manager.js';
|
|
9
11
|
export function registerAssessCommand(program) {
|
|
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');
|
|
12
|
+
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').option('--show-config-sources', 'Display all configuration sources and merged result');
|
|
11
13
|
|
|
12
14
|
// Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
|
|
13
15
|
StandardOptions.define(command).action(async (servicePath, options) => {
|
|
14
16
|
try {
|
|
15
17
|
const output = new (await import('../lib/shared/utils/output-formatter.js')).OutputFormatter(options);
|
|
16
|
-
const
|
|
18
|
+
const configManager = new ServiceConfigManager({
|
|
17
19
|
verbose: options.verbose,
|
|
18
20
|
quiet: options.quiet,
|
|
19
|
-
json: options.json
|
|
21
|
+
json: options.json,
|
|
22
|
+
showSources: options.showConfigSources
|
|
20
23
|
});
|
|
24
|
+
const orchestrator = new ServiceOrchestrator();
|
|
25
|
+
const targetPath = servicePath || process.cwd();
|
|
21
26
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// Validate service path with better error handling
|
|
28
|
+
try {
|
|
29
|
+
servicePath = await configManager.validateServicePath(targetPath, orchestrator);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
output.error(error.message);
|
|
32
|
+
if (error.suggestions) {
|
|
33
|
+
output.info('Suggestions:');
|
|
34
|
+
output.list(error.suggestions);
|
|
28
35
|
}
|
|
36
|
+
process.exit(1);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
// Load and merge all configurations
|
|
40
|
+
const mergedOptions = await configManager.loadServiceConfig(servicePath, options, {
|
|
41
|
+
export: null,
|
|
42
|
+
domain: null,
|
|
43
|
+
serviceType: null,
|
|
44
|
+
token: null
|
|
45
|
+
});
|
|
46
|
+
let assessment; // Declare here so it's available in the entire scope
|
|
36
47
|
|
|
37
48
|
// Try to load professional orchestration package
|
|
38
49
|
let orchestrationModule;
|
|
@@ -45,8 +56,6 @@ export function registerAssessCommand(program) {
|
|
|
45
56
|
output.info('💡 Using basic assessment capabilities');
|
|
46
57
|
output.info('💡 For advanced assessment: npm install @tamyla/clodo-orchestration');
|
|
47
58
|
}
|
|
48
|
-
let assessment;
|
|
49
|
-
const targetPath = servicePath || process.cwd();
|
|
50
59
|
if (hasEnterprisePackage) {
|
|
51
60
|
output.section('Professional Capability Assessment');
|
|
52
61
|
output.list([`Service Path: ${targetPath}`, mergedOptions.domain ? `Domain: ${mergedOptions.domain}` : null, mergedOptions.serviceType ? `Service Type: ${mergedOptions.serviceType}` : null, 'Enterprise Package: ✅ Available'].filter(Boolean));
|
|
@@ -19,6 +19,7 @@ import { CloudflareServiceValidator } from '../lib/shared/config/cloudflare-serv
|
|
|
19
19
|
import { DeploymentCredentialCollector } from '../lib/shared/deployment/credential-collector.js';
|
|
20
20
|
import { StandardOptions } from '../lib/shared/utils/cli-options.js';
|
|
21
21
|
import { ConfigLoader } from '../lib/shared/utils/config-loader.js';
|
|
22
|
+
import { ServiceConfigManager } from '../lib/shared/utils/service-config-manager.js';
|
|
22
23
|
import { DomainRouter } from '../lib/shared/routing/domain-router.js';
|
|
23
24
|
import { MultiDomainOrchestrator } from '../orchestration/multi-domain-orchestrator.js';
|
|
24
25
|
|
|
@@ -30,7 +31,7 @@ import { handleDeploymentError } from './helpers/error-recovery.js';
|
|
|
30
31
|
export function registerDeployCommand(program) {
|
|
31
32
|
const command = program.command('deploy').description('Deploy a Clodo service with smart credential handling and domain selection')
|
|
32
33
|
// Cloudflare-specific options
|
|
33
|
-
.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('--development', 'Deploy to development environment (shorthand for --environment development)').option('--staging', 'Deploy to staging environment (shorthand for --environment staging)').option('--production', 'Deploy to production environment (shorthand for --environment production)').option('--all-domains', 'Deploy to all configured domains (ignores --domain flag)').option('--dry-run', 'Simulate deployment without making changes').option('-y, --yes', 'Skip confirmation prompts (for CI/CD)').option('--service-path <path>', 'Path to service directory', '.');
|
|
34
|
+
.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('--development', 'Deploy to development environment (shorthand for --environment development)').option('--staging', 'Deploy to staging environment (shorthand for --environment staging)').option('--production', 'Deploy to production environment (shorthand for --environment production)').option('--all-domains', 'Deploy to all configured domains (ignores --domain flag)').option('--dry-run', 'Simulate deployment without making changes').option('-y, --yes', 'Skip confirmation prompts (for CI/CD)').option('--service-path <path>', 'Path to service directory', '.').option('--show-config-sources', 'Display all configuration sources and merged result');
|
|
34
35
|
|
|
35
36
|
// Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
|
|
36
37
|
StandardOptions.define(command).action(async options => {
|
|
@@ -60,15 +61,36 @@ export function registerDeployCommand(program) {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
//
|
|
64
|
-
|
|
64
|
+
// Use ServiceConfigManager for standardized config loading
|
|
65
|
+
const configManager = new ServiceConfigManager({
|
|
66
|
+
verbose: options.verbose,
|
|
67
|
+
quiet: options.quiet,
|
|
68
|
+
json: options.json,
|
|
69
|
+
showSources: options.showConfigSources
|
|
70
|
+
});
|
|
65
71
|
|
|
66
|
-
//
|
|
67
|
-
const
|
|
72
|
+
// For deploy command, we use current directory as service path
|
|
73
|
+
const deployServicePath = resolve(options.servicePath || '.');
|
|
74
|
+
|
|
75
|
+
// Load and merge all configurations
|
|
76
|
+
const mergedOptions = await configManager.loadServiceConfig(deployServicePath, {
|
|
77
|
+
...options,
|
|
78
|
+
configFile: options.configFile // Pass through the config file option
|
|
79
|
+
}, {
|
|
80
|
+
token: null,
|
|
81
|
+
accountId: null,
|
|
82
|
+
zoneId: null,
|
|
83
|
+
domain: null,
|
|
84
|
+
environment: 'production',
|
|
85
|
+
allDomains: false,
|
|
86
|
+
dryRun: false,
|
|
87
|
+
yes: false,
|
|
88
|
+
servicePath: '.'
|
|
89
|
+
});
|
|
68
90
|
output.info('🚀 Clodo Service Deployment');
|
|
69
91
|
|
|
70
92
|
// Step 1: Load and validate service configuration
|
|
71
|
-
const servicePath = resolve(mergedOptions.servicePath ||
|
|
93
|
+
const servicePath = resolve(mergedOptions.servicePath || deployServicePath);
|
|
72
94
|
const serviceConfig = await ManifestLoader.loadAndValidateCloudflareService(servicePath);
|
|
73
95
|
if (!serviceConfig.manifest) {
|
|
74
96
|
if (serviceConfig.error === 'NOT_A_CLOUDFLARE_SERVICE') {
|
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
import { ServiceOrchestrator } from '../service-management/ServiceOrchestrator.js';
|
|
3
4
|
import { StandardOptions } from '../lib/shared/utils/cli-options.js';
|
|
4
|
-
import {
|
|
5
|
+
import { ServiceConfigManager } from '../lib/shared/utils/service-config-manager.js';
|
|
5
6
|
export function registerDiagnoseCommand(program) {
|
|
6
|
-
const command = 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').option('--fix-suggestions', 'Include suggested fixes for issues');
|
|
7
|
+
const command = 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').option('--fix-suggestions', 'Include suggested fixes for issues').option('--show-config-sources', 'Display all configuration sources and merged result');
|
|
7
8
|
|
|
8
9
|
// Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
|
|
9
10
|
StandardOptions.define(command).action(async (servicePath, options) => {
|
|
10
11
|
try {
|
|
11
12
|
const output = new (await import('../lib/shared/utils/output-formatter.js')).OutputFormatter(options);
|
|
12
|
-
const
|
|
13
|
+
const configManager = new ServiceConfigManager({
|
|
13
14
|
verbose: options.verbose,
|
|
14
15
|
quiet: options.quiet,
|
|
15
|
-
json: options.json
|
|
16
|
+
json: options.json,
|
|
17
|
+
showSources: options.showConfigSources
|
|
16
18
|
});
|
|
17
|
-
|
|
18
|
-
// Load config from file if specified
|
|
19
|
-
let configFileData = {};
|
|
20
|
-
if (options.configFile) {
|
|
21
|
-
configFileData = configLoader.loadSafe(options.configFile, {});
|
|
22
|
-
if (options.verbose && !options.quiet) {
|
|
23
|
-
output.info(`Loaded configuration from: ${options.configFile}`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Merge config file defaults with CLI options (CLI takes precedence)
|
|
28
|
-
const mergedOptions = configLoader.merge(configFileData, options);
|
|
29
19
|
const orchestrator = new ServiceOrchestrator();
|
|
30
20
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
servicePath = await
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
// Validate service path with better error handling
|
|
22
|
+
try {
|
|
23
|
+
servicePath = await configManager.validateServicePath(servicePath, orchestrator);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
output.error(error.message);
|
|
26
|
+
if (error.suggestions) {
|
|
27
|
+
output.info('Suggestions:');
|
|
28
|
+
output.list(error.suggestions);
|
|
37
29
|
}
|
|
30
|
+
process.exit(1);
|
|
38
31
|
}
|
|
32
|
+
|
|
33
|
+
// Load and merge all configurations
|
|
34
|
+
const mergedOptions = await configManager.loadServiceConfig(servicePath, options, {
|
|
35
|
+
deepScan: false,
|
|
36
|
+
exportReport: null,
|
|
37
|
+
fixSuggestions: false
|
|
38
|
+
});
|
|
39
39
|
output.info('🔍 Diagnosing service...');
|
|
40
40
|
const diagnosis = await orchestrator.diagnoseService(servicePath, mergedOptions);
|
|
41
41
|
|
|
@@ -3,36 +3,61 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
+
import path from 'path';
|
|
6
7
|
import { ServiceOrchestrator } from '../service-management/ServiceOrchestrator.js';
|
|
7
8
|
import { StandardOptions } from '../lib/shared/utils/cli-options.js';
|
|
8
|
-
import {
|
|
9
|
+
import { ServiceConfigManager } from '../lib/shared/utils/service-config-manager.js';
|
|
9
10
|
export function registerUpdateCommand(program) {
|
|
10
|
-
const command = 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').option('--preview', 'Show what would be changed without applying').option('--force', 'Skip confirmation prompts');
|
|
11
|
+
const command = 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').option('--preview', 'Show what would be changed without applying').option('--show-config-sources', 'Display all configuration sources and merged result').option('--force', 'Skip confirmation prompts');
|
|
11
12
|
|
|
12
13
|
// Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
|
|
13
14
|
StandardOptions.define(command).action(async (servicePath, options) => {
|
|
14
15
|
try {
|
|
15
16
|
const output = new (await import('../lib/shared/utils/output-formatter.js')).OutputFormatter(options);
|
|
16
|
-
const
|
|
17
|
+
const configManager = new ServiceConfigManager({
|
|
17
18
|
verbose: options.verbose,
|
|
18
19
|
quiet: options.quiet,
|
|
19
|
-
json: options.json
|
|
20
|
+
json: options.json,
|
|
21
|
+
showSources: options.showConfigSources
|
|
20
22
|
});
|
|
23
|
+
const orchestrator = new ServiceOrchestrator();
|
|
21
24
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// Auto-detect service path if not provided
|
|
26
|
+
if (!servicePath) {
|
|
27
|
+
servicePath = await orchestrator.detectServicePath();
|
|
28
|
+
if (!servicePath) {
|
|
29
|
+
output.error('No service path provided and could not auto-detect service directory');
|
|
30
|
+
process.exit(1);
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
// Validate service path with better error handling
|
|
35
|
+
try {
|
|
36
|
+
servicePath = await configManager.validateServicePath(servicePath, orchestrator);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
output.error(error.message);
|
|
39
|
+
if (error.suggestions) {
|
|
40
|
+
output.info('Suggestions:');
|
|
41
|
+
output.list(error.suggestions);
|
|
42
|
+
}
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
34
45
|
|
|
35
|
-
//
|
|
46
|
+
// Load and merge all configurations
|
|
47
|
+
const mergedOptions = await configManager.loadServiceConfig(servicePath, options, {
|
|
48
|
+
interactive: false,
|
|
49
|
+
domainName: null,
|
|
50
|
+
cloudflareToken: null,
|
|
51
|
+
cloudflareAccountId: null,
|
|
52
|
+
cloudflareZoneId: null,
|
|
53
|
+
environment: null,
|
|
54
|
+
addFeature: null,
|
|
55
|
+
removeFeature: null,
|
|
56
|
+
regenerateConfigs: false,
|
|
57
|
+
fixErrors: false,
|
|
58
|
+
preview: false,
|
|
59
|
+
force: false
|
|
60
|
+
});
|
|
36
61
|
if (!servicePath) {
|
|
37
62
|
servicePath = await orchestrator.detectServicePath();
|
|
38
63
|
if (!servicePath) {
|
|
@@ -44,7 +69,9 @@ export function registerUpdateCommand(program) {
|
|
|
44
69
|
}
|
|
45
70
|
|
|
46
71
|
// Validate it's a service directory
|
|
47
|
-
const isValid = await orchestrator.validateService(servicePath
|
|
72
|
+
const isValid = await orchestrator.validateService(servicePath, {
|
|
73
|
+
customConfig: mergedOptions
|
|
74
|
+
});
|
|
48
75
|
if (!isValid.valid) {
|
|
49
76
|
output.warning('Service has configuration issues. Use --fix-errors to attempt automatic fixes.');
|
|
50
77
|
if (!mergedOptions.fixErrors) {
|
|
@@ -3,37 +3,46 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
+
import path from 'path';
|
|
6
7
|
import { ServiceOrchestrator } from '../service-management/ServiceOrchestrator.js';
|
|
7
8
|
import { StandardOptions } from '../lib/shared/utils/cli-options.js';
|
|
8
|
-
import {
|
|
9
|
+
import { ServiceConfigManager } from '../lib/shared/utils/service-config-manager.js';
|
|
9
10
|
export function registerValidateCommand(program) {
|
|
10
|
-
const command = program.command('validate <service-path>').description('Validate an existing service configuration').option('--deep-scan', 'Run comprehensive validation checks').option('--export-report <file>', 'Export validation report to JSON file');
|
|
11
|
+
const command = program.command('validate <service-path>').description('Validate an existing service configuration').option('--deep-scan', 'Run comprehensive validation checks').option('--export-report <file>', 'Export validation report to JSON file').option('--show-config-sources', 'Display all configuration sources and merged result');
|
|
11
12
|
|
|
12
13
|
// Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
|
|
13
14
|
StandardOptions.define(command).action(async (servicePath, options) => {
|
|
14
15
|
try {
|
|
15
16
|
const output = new (await import('../lib/shared/utils/output-formatter.js')).OutputFormatter(options);
|
|
16
|
-
const
|
|
17
|
+
const configManager = new ServiceConfigManager({
|
|
17
18
|
verbose: options.verbose,
|
|
18
19
|
quiet: options.quiet,
|
|
19
|
-
json: options.json
|
|
20
|
+
json: options.json,
|
|
21
|
+
showSources: options.showConfigSources
|
|
20
22
|
});
|
|
23
|
+
const orchestrator = new ServiceOrchestrator();
|
|
21
24
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// Validate service path with better error handling
|
|
26
|
+
try {
|
|
27
|
+
servicePath = await configManager.validateServicePath(servicePath, orchestrator);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
output.error(error.message);
|
|
30
|
+
if (error.suggestions) {
|
|
31
|
+
output.info('Suggestions:');
|
|
32
|
+
output.list(error.suggestions);
|
|
28
33
|
}
|
|
34
|
+
process.exit(1);
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
//
|
|
32
|
-
const mergedOptions =
|
|
33
|
-
|
|
37
|
+
// Load and merge all configurations
|
|
38
|
+
const mergedOptions = await configManager.loadServiceConfig(servicePath, options, {
|
|
39
|
+
deepScan: false,
|
|
40
|
+
exportReport: null
|
|
41
|
+
});
|
|
34
42
|
const result = await orchestrator.validateService(servicePath, {
|
|
35
43
|
deepScan: mergedOptions.deepScan,
|
|
36
|
-
exportReport: mergedOptions.exportReport
|
|
44
|
+
exportReport: mergedOptions.exportReport,
|
|
45
|
+
customConfig: mergedOptions.validation // Pass custom validation config
|
|
37
46
|
});
|
|
38
47
|
if (result.valid) {
|
|
39
48
|
output.success('Service configuration is valid');
|
|
@@ -42,6 +51,11 @@ export function registerValidateCommand(program) {
|
|
|
42
51
|
output.list(result.issues || []);
|
|
43
52
|
process.exit(1);
|
|
44
53
|
}
|
|
54
|
+
|
|
55
|
+
// Report export success if requested
|
|
56
|
+
if (mergedOptions.exportReport) {
|
|
57
|
+
output.success(`📄 Report exported to: ${mergedOptions.exportReport}`);
|
|
58
|
+
}
|
|
45
59
|
} catch (error) {
|
|
46
60
|
const output = new (await import('../lib/shared/utils/output-formatter.js')).OutputFormatter(options || {});
|
|
47
61
|
output.error(`Validation failed: ${error.message}`);
|
|
@@ -276,13 +276,13 @@ export class ManifestLoader {
|
|
|
276
276
|
*/
|
|
277
277
|
static printManifestInfo(manifest) {
|
|
278
278
|
console.log(chalk.cyan('\n📋 Service Configuration:\n'));
|
|
279
|
-
console.log(chalk.white(`Service Name: ${chalk.bold(manifest.serviceName)}`));
|
|
280
|
-
console.log(chalk.white(`Service Type: ${chalk.bold(manifest.serviceType)}`));
|
|
279
|
+
console.log(chalk.white(`Service Name: ${chalk.bold(manifest.service?.name || manifest.serviceName || 'unknown')}`));
|
|
280
|
+
console.log(chalk.white(`Service Type: ${chalk.bold(manifest.service?.type || manifest.serviceType || 'unknown')}`));
|
|
281
281
|
console.log(chalk.white(`Source: ${chalk.gray(manifest._source)}`));
|
|
282
282
|
if (manifest._legacyNote) {
|
|
283
283
|
console.log(chalk.yellow(`\nℹ️ ${manifest._legacyNote}`));
|
|
284
284
|
}
|
|
285
|
-
if (manifest.deployment.domains) {
|
|
285
|
+
if (manifest.deployment && manifest.deployment.domains) {
|
|
286
286
|
console.log(chalk.white(`\nDomains:`));
|
|
287
287
|
manifest.deployment.domains.forEach(domain => {
|
|
288
288
|
console.log(chalk.gray(` - ${domain.name} (${domain.environment})`));
|
|
@@ -114,8 +114,9 @@ export class ConfigLoader {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
117
|
+
* Deep merge configuration file defaults with CLI options
|
|
118
118
|
* CLI options take precedence over config file defaults
|
|
119
|
+
* Handles nested objects properly (deep merge)
|
|
119
120
|
* @param {Object} configFile - Configuration loaded from file
|
|
120
121
|
* @param {Object} cliOptions - Options from command line
|
|
121
122
|
* @returns {Object} Merged configuration with CLI options taking precedence
|
|
@@ -128,22 +129,57 @@ export class ConfigLoader {
|
|
|
128
129
|
return configFile || {};
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
//
|
|
132
|
-
const merged =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Only override with CLI option if it's explicitly provided (not undefined, null, or empty string)
|
|
137
|
-
if (value !== undefined && value !== null && value !== '') {
|
|
138
|
-
merged[key] = value;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
132
|
+
// Start with deep copy of config file
|
|
133
|
+
const merged = this.deepClone(configFile);
|
|
134
|
+
|
|
135
|
+
// Deep merge CLI options on top
|
|
136
|
+
this.deepMergeInto(merged, cliOptions);
|
|
141
137
|
if (this.verbose && !this.quiet) {
|
|
142
138
|
console.log('📋 Configuration merged: CLI options override file defaults');
|
|
143
139
|
}
|
|
144
140
|
return merged;
|
|
145
141
|
}
|
|
146
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Deep clone an object
|
|
145
|
+
*/
|
|
146
|
+
deepClone(obj) {
|
|
147
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
148
|
+
if (obj instanceof Date) return new Date(obj.getTime());
|
|
149
|
+
if (obj instanceof Array) return obj.map(item => this.deepClone(item));
|
|
150
|
+
if (typeof obj === 'object') {
|
|
151
|
+
const cloned = {};
|
|
152
|
+
for (const key in obj) {
|
|
153
|
+
if (obj.hasOwnProperty(key)) {
|
|
154
|
+
cloned[key] = this.deepClone(obj[key]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return cloned;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Deep merge source into target (modifies target)
|
|
163
|
+
*/
|
|
164
|
+
deepMergeInto(target, source) {
|
|
165
|
+
if (!source || typeof source !== 'object') return;
|
|
166
|
+
for (const [key, value] of Object.entries(source)) {
|
|
167
|
+
// Only override if value is explicitly provided (not undefined, null, or empty string)
|
|
168
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
169
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
170
|
+
// Deep merge nested objects
|
|
171
|
+
if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
|
|
172
|
+
target[key] = {};
|
|
173
|
+
}
|
|
174
|
+
this.deepMergeInto(target[key], value);
|
|
175
|
+
} else {
|
|
176
|
+
// Override with source value (including arrays and primitives)
|
|
177
|
+
target[key] = value;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
147
183
|
/**
|
|
148
184
|
* Substitute environment variables in configuration values
|
|
149
185
|
* Format: ${ENV_VAR_NAME} in strings will be replaced with environment variable values
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Config Manager
|
|
3
|
+
* Centralized, reusable configuration loading for CLI commands
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Standardized config file discovery across all commands
|
|
7
|
+
* - Proper error handling with user-friendly messages
|
|
8
|
+
* - Deep merging of nested configuration objects
|
|
9
|
+
* - Config validation against schemas
|
|
10
|
+
* - Debugging support with --show-config-sources
|
|
11
|
+
* - Environment variable substitution
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { ConfigLoader } from './config-loader.js';
|
|
17
|
+
export class ServiceConfigManager {
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.configLoader = new ConfigLoader(options);
|
|
20
|
+
this.verbose = options.verbose || false;
|
|
21
|
+
this.quiet = options.quiet || false;
|
|
22
|
+
this.json = options.json || false;
|
|
23
|
+
this.showSources = options.showSources || false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Load and merge configuration for a service command
|
|
28
|
+
* @param {string} servicePath - Path to the service directory
|
|
29
|
+
* @param {Object} cliOptions - CLI options from commander
|
|
30
|
+
* @param {Object} commandDefaults - Default options for this command
|
|
31
|
+
* @returns {Object} Merged configuration
|
|
32
|
+
*/
|
|
33
|
+
async loadServiceConfig(servicePath, cliOptions = {}, commandDefaults = {}) {
|
|
34
|
+
const sources = [];
|
|
35
|
+
|
|
36
|
+
// 1. Load framework defaults (always available)
|
|
37
|
+
const frameworkConfig = this.loadFrameworkDefaults();
|
|
38
|
+
sources.push({
|
|
39
|
+
name: 'Framework Defaults',
|
|
40
|
+
path: 'config/validation-config.json',
|
|
41
|
+
config: frameworkConfig,
|
|
42
|
+
priority: 4
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 2. Load service-specific config (highest priority for customization)
|
|
46
|
+
const serviceConfig = await this.loadServiceConfigFile(servicePath);
|
|
47
|
+
if (serviceConfig) {
|
|
48
|
+
sources.push({
|
|
49
|
+
name: 'Service Config',
|
|
50
|
+
path: path.join(servicePath, 'validation-config.json'),
|
|
51
|
+
config: serviceConfig,
|
|
52
|
+
priority: 1
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 3. Load current directory config (for deploy command compatibility)
|
|
57
|
+
const cwdConfig = this.loadCwdConfigFile();
|
|
58
|
+
if (cwdConfig) {
|
|
59
|
+
sources.push({
|
|
60
|
+
name: 'Current Directory Config',
|
|
61
|
+
path: path.join(process.cwd(), 'validation-config.json'),
|
|
62
|
+
config: cwdConfig,
|
|
63
|
+
priority: 2
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 4. Load explicitly specified config file
|
|
68
|
+
let explicitConfig = {};
|
|
69
|
+
if (cliOptions.configFile) {
|
|
70
|
+
explicitConfig = this.configLoader.loadSafe(cliOptions.configFile, {});
|
|
71
|
+
if (Object.keys(explicitConfig).length > 0) {
|
|
72
|
+
sources.push({
|
|
73
|
+
name: 'Explicit Config File',
|
|
74
|
+
path: cliOptions.configFile,
|
|
75
|
+
config: explicitConfig,
|
|
76
|
+
priority: 0 // Highest priority
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Sort sources by priority (lower number = higher priority)
|
|
82
|
+
sources.sort((a, b) => a.priority - b.priority);
|
|
83
|
+
|
|
84
|
+
// Show config sources if requested
|
|
85
|
+
if (this.showSources) {
|
|
86
|
+
this.displayConfigSources(sources);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Deep merge all configurations
|
|
90
|
+
let mergedConfig = {
|
|
91
|
+
...commandDefaults
|
|
92
|
+
};
|
|
93
|
+
for (const source of sources) {
|
|
94
|
+
mergedConfig = this.deepMerge(mergedConfig, source.config);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// CLI options override everything (except when showing sources)
|
|
98
|
+
if (!this.showSources) {
|
|
99
|
+
mergedConfig = this.deepMerge(mergedConfig, cliOptions);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Substitute environment variables
|
|
103
|
+
mergedConfig = this.configLoader.substituteEnvironmentVariables(mergedConfig);
|
|
104
|
+
return mergedConfig;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Load framework default configuration
|
|
109
|
+
*/
|
|
110
|
+
loadFrameworkDefaults() {
|
|
111
|
+
try {
|
|
112
|
+
const frameworkConfigPath = path.join(process.cwd(), 'config', 'validation-config.json');
|
|
113
|
+
return this.configLoader.loadSafe(frameworkConfigPath, {});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Framework config is optional, return empty object
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Load service-specific configuration file
|
|
122
|
+
*/
|
|
123
|
+
async loadServiceConfigFile(servicePath) {
|
|
124
|
+
if (!servicePath) return null;
|
|
125
|
+
const configPath = path.join(servicePath, 'validation-config.json');
|
|
126
|
+
try {
|
|
127
|
+
if (!existsSync(configPath)) {
|
|
128
|
+
return null; // File doesn't exist, which is fine
|
|
129
|
+
}
|
|
130
|
+
const config = this.configLoader.load(configPath);
|
|
131
|
+
if (this.verbose && !this.quiet) {
|
|
132
|
+
console.log(`✅ Loaded service config: ${configPath}`);
|
|
133
|
+
}
|
|
134
|
+
return config;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// This is an actual error (file exists but can't be loaded)
|
|
137
|
+
if (!this.quiet) {
|
|
138
|
+
console.warn(`⚠️ Failed to load service config from ${configPath}: ${error.message}`);
|
|
139
|
+
console.warn(` Using framework defaults instead.`);
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Load configuration from current working directory
|
|
147
|
+
*/
|
|
148
|
+
loadCwdConfigFile() {
|
|
149
|
+
const configPath = path.join(process.cwd(), 'validation-config.json');
|
|
150
|
+
try {
|
|
151
|
+
if (!existsSync(configPath)) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const config = this.configLoader.load(configPath);
|
|
155
|
+
if (this.verbose && !this.quiet) {
|
|
156
|
+
console.log(`✅ Loaded current directory config: ${configPath}`);
|
|
157
|
+
}
|
|
158
|
+
return config;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (!this.quiet) {
|
|
161
|
+
console.warn(`⚠️ Failed to load current directory config from ${configPath}: ${error.message}`);
|
|
162
|
+
console.warn(` Using framework defaults instead.`);
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Deep merge two configuration objects
|
|
170
|
+
*/
|
|
171
|
+
deepMerge(target, source) {
|
|
172
|
+
if (!source || typeof source !== 'object') return target;
|
|
173
|
+
if (!target || typeof target !== 'object') return source;
|
|
174
|
+
const result = {
|
|
175
|
+
...target
|
|
176
|
+
};
|
|
177
|
+
for (const [key, value] of Object.entries(source)) {
|
|
178
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
179
|
+
// Deep merge nested objects
|
|
180
|
+
result[key] = this.deepMerge(result[key] || {}, value);
|
|
181
|
+
} else {
|
|
182
|
+
// Override with source value (including arrays and primitives)
|
|
183
|
+
result[key] = value;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Display configuration sources for debugging
|
|
191
|
+
*/
|
|
192
|
+
displayConfigSources(sources) {
|
|
193
|
+
console.log('\n📋 Configuration Sources (in merge order):');
|
|
194
|
+
for (const source of sources) {
|
|
195
|
+
const status = Object.keys(source.config).length > 0 ? '✅ Loaded' : '❌ Empty';
|
|
196
|
+
console.log(` ${status} ${source.name}: ${source.path}`);
|
|
197
|
+
}
|
|
198
|
+
console.log('\n📋 Final merged configuration:');
|
|
199
|
+
console.log(JSON.stringify(this.deepMerge({}, ...sources.map(s => s.config)), null, 2));
|
|
200
|
+
console.log('');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Validate service path exists and is accessible
|
|
205
|
+
*/
|
|
206
|
+
async validateServicePath(servicePath, orchestrator) {
|
|
207
|
+
if (!servicePath) {
|
|
208
|
+
// Try auto-detection
|
|
209
|
+
servicePath = await orchestrator.detectServicePath();
|
|
210
|
+
if (!servicePath) {
|
|
211
|
+
const error = new Error('No service path provided and could not auto-detect service directory');
|
|
212
|
+
error.suggestions = ['Ensure you are in a service directory or specify --service-path', 'Service directories must contain: package.json, src/config/domains.js, wrangler.toml', 'Try: cd /path/to/your/service && clodo-service <command>'];
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Validate the path exists and is accessible
|
|
218
|
+
try {
|
|
219
|
+
await orchestrator.isServiceDirectory(servicePath);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const validationError = new Error(`Invalid service path: ${servicePath}`);
|
|
222
|
+
validationError.suggestions = ['Ensure the directory exists and is accessible', 'Service directories must contain: package.json, src/config/domains.js, wrangler.toml', `Check permissions for: ${servicePath}`];
|
|
223
|
+
throw validationError;
|
|
224
|
+
}
|
|
225
|
+
return servicePath;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -18,6 +18,7 @@ import { ValidationHandler } from './handlers/ValidationHandler.js';
|
|
|
18
18
|
import { ErrorTracker } from './ErrorTracker.js';
|
|
19
19
|
import chalk from 'chalk';
|
|
20
20
|
import fs from 'fs/promises';
|
|
21
|
+
import fsSync from 'fs';
|
|
21
22
|
import path from 'path';
|
|
22
23
|
export class ServiceOrchestrator {
|
|
23
24
|
constructor(options = {}) {
|
|
@@ -97,8 +98,18 @@ export class ServiceOrchestrator {
|
|
|
97
98
|
/**
|
|
98
99
|
* Validate an existing service configuration
|
|
99
100
|
*/
|
|
100
|
-
async validateService(servicePath) {
|
|
101
|
-
|
|
101
|
+
async validateService(servicePath, options = {}) {
|
|
102
|
+
// Create ValidationHandler with custom config if provided
|
|
103
|
+
const validationHandler = options.customConfig ? new ValidationHandler({
|
|
104
|
+
customConfig: options.customConfig
|
|
105
|
+
}) : this.validationHandler;
|
|
106
|
+
const result = await validationHandler.validateService(servicePath);
|
|
107
|
+
|
|
108
|
+
// Export report if requested
|
|
109
|
+
if (options.exportReport) {
|
|
110
|
+
await this.exportValidationReport(result, options.exportReport);
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
/**
|
|
@@ -554,6 +565,13 @@ export class ServiceOrchestrator {
|
|
|
554
565
|
* Export diagnostic report
|
|
555
566
|
*/
|
|
556
567
|
async exportDiagnosticReport(diagnosis, filePath) {
|
|
568
|
+
// Ensure directory exists
|
|
569
|
+
const dir = path.dirname(filePath);
|
|
570
|
+
if (!fsSync.existsSync(dir)) {
|
|
571
|
+
fsSync.mkdirSync(dir, {
|
|
572
|
+
recursive: true
|
|
573
|
+
});
|
|
574
|
+
}
|
|
557
575
|
const report = {
|
|
558
576
|
timestamp: new Date().toISOString(),
|
|
559
577
|
serviceName: diagnosis.serviceName,
|
|
@@ -569,6 +587,29 @@ export class ServiceOrchestrator {
|
|
|
569
587
|
await fs.writeFile(filePath, JSON.stringify(report, null, 2), 'utf8');
|
|
570
588
|
}
|
|
571
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Export validation report
|
|
592
|
+
*/
|
|
593
|
+
async exportValidationReport(validation, filePath) {
|
|
594
|
+
// Ensure directory exists
|
|
595
|
+
const dir = path.dirname(filePath);
|
|
596
|
+
if (!fsSync.existsSync(dir)) {
|
|
597
|
+
fsSync.mkdirSync(dir, {
|
|
598
|
+
recursive: true
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
const report = {
|
|
602
|
+
timestamp: new Date().toISOString(),
|
|
603
|
+
servicePath: validation.servicePath || 'unknown',
|
|
604
|
+
valid: validation.valid,
|
|
605
|
+
summary: {
|
|
606
|
+
issues: validation.issues ? validation.issues.length : 0
|
|
607
|
+
},
|
|
608
|
+
issues: validation.issues || []
|
|
609
|
+
};
|
|
610
|
+
await fs.writeFile(filePath, JSON.stringify(report, null, 2), 'utf8');
|
|
611
|
+
}
|
|
612
|
+
|
|
572
613
|
/**
|
|
573
614
|
* Generate service using legacy ServiceCreator (placeholder for GenerationEngine)
|
|
574
615
|
*/
|
|
@@ -8,6 +8,18 @@ import path from 'path';
|
|
|
8
8
|
export class ValidationHandler {
|
|
9
9
|
constructor(options = {}) {
|
|
10
10
|
this.strict = options.strict || false;
|
|
11
|
+
this.customConfig = options.customConfig || {};
|
|
12
|
+
|
|
13
|
+
// Use custom validation config if provided, otherwise use defaults
|
|
14
|
+
this.validationConfig = {
|
|
15
|
+
requiredFiles: this.customConfig.requiredFiles || ['package.json', 'src/config/domains.js', 'src/worker/index.js', 'wrangler.toml'],
|
|
16
|
+
optionalFiles: this.customConfig.optionalFiles || ['README.md', 'LICENSE', '.gitignore'],
|
|
17
|
+
requiredFields: this.customConfig.requiredFields || {
|
|
18
|
+
'package.json': ['name', 'version', 'type', 'main'],
|
|
19
|
+
'wrangler.toml': ['name', 'main', 'compatibility_date']
|
|
20
|
+
},
|
|
21
|
+
serviceTypes: this.customConfig.serviceTypes || ['data-service', 'auth-service', 'content-service', 'api-gateway', 'static-site', 'generic']
|
|
22
|
+
};
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
/**
|
|
@@ -16,9 +28,8 @@ export class ValidationHandler {
|
|
|
16
28
|
async validateService(servicePath) {
|
|
17
29
|
const issues = [];
|
|
18
30
|
|
|
19
|
-
// Check for required files
|
|
20
|
-
const
|
|
21
|
-
for (const file of requiredFiles) {
|
|
31
|
+
// Check for required files using custom config
|
|
32
|
+
for (const file of this.validationConfig.requiredFiles) {
|
|
22
33
|
const filePath = path.join(servicePath, file);
|
|
23
34
|
try {
|
|
24
35
|
await fs.access(filePath);
|
|
@@ -53,12 +64,14 @@ export class ValidationHandler {
|
|
|
53
64
|
const packageJsonPath = path.join(servicePath, 'package.json');
|
|
54
65
|
try {
|
|
55
66
|
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
|
|
68
|
+
// Check custom required fields for package.json
|
|
69
|
+
const requiredPackageFields = this.validationConfig.requiredFields['package.json'] || ['name', 'version'];
|
|
70
|
+
requiredPackageFields.forEach(field => {
|
|
71
|
+
if (!packageJson[field]) {
|
|
72
|
+
issues.push(`package.json: Missing required field: ${field}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
62
75
|
if (!packageJson.type || packageJson.type !== 'module') {
|
|
63
76
|
issues.push('package.json: Should use "type": "module" for ES modules');
|
|
64
77
|
}
|