@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 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 { ConfigLoader } from '../lib/shared/utils/config-loader.js';
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 configLoader = new ConfigLoader({
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
- // 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}`);
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
- // 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);
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
- // Substitute environment variables in config
64
- configFileData = configLoader.substituteEnvironmentVariables(configFileData);
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
- // Merge config file defaults with CLI options (CLI takes precedence)
67
- const mergedOptions = configLoader.merge(configFileData, options);
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 { ConfigLoader } from '../lib/shared/utils/config-loader.js';
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 configLoader = new ConfigLoader({
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
- // Auto-detect service path if not provided
32
- if (!servicePath) {
33
- servicePath = await orchestrator.detectServicePath();
34
- if (!servicePath) {
35
- output.error('No service path provided and could not auto-detect service directory');
36
- process.exit(1);
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 { ConfigLoader } from '../lib/shared/utils/config-loader.js';
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 configLoader = new ConfigLoader({
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
- // 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}`);
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
- // Merge config file defaults with CLI options (CLI takes precedence)
32
- const mergedOptions = configLoader.merge(configFileData, options);
33
- const orchestrator = new ServiceOrchestrator();
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
- // Auto-detect service path if not provided
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 { ConfigLoader } from '../lib/shared/utils/config-loader.js';
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 configLoader = new ConfigLoader({
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
- // 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}`);
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
- // Merge config file defaults with CLI options (CLI takes precedence)
32
- const mergedOptions = configLoader.merge(configFileData, options);
33
- const orchestrator = new ServiceOrchestrator();
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
- * Merge configuration file defaults with CLI options
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
- // Create merged result: start with config file, override with CLI options
132
- const merged = {
133
- ...configFile
134
- };
135
- for (const [key, value] of Object.entries(cliOptions)) {
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
- return await this.validationHandler.validateService(servicePath);
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 requiredFiles = ['package.json', 'src/config/domains.js', 'src/worker/index.js', 'wrangler.toml'];
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
- if (!packageJson.name) {
57
- issues.push('package.json: Missing name field');
58
- }
59
- if (!packageJson.version) {
60
- issues.push('package.json: Missing version field');
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "3.1.23",
3
+ "version": "3.1.24",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [