@tamyla/clodo-framework 4.0.14 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # [4.1.0](https://github.com/tamylaa/clodo-framework/compare/v4.0.15...v4.1.0) (2026-01-31)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **lint:** remove unnecessary escape in serviceName regex to satisfy no-useless-escape ([ac4dcda](https://github.com/tamylaa/clodo-framework/commit/ac4dcdabb39dda3dba3f4e06022f28936a53d645))
7
+
8
+
9
+ ### Features
10
+
11
+ * **programmatic-service-api:** programmatic createService + configurable validation ([#4](https://github.com/tamylaa/clodo-framework/issues/4)) ([040f68b](https://github.com/tamylaa/clodo-framework/commit/040f68b9115b604b3f27d7baa14c63122494c5f3))
12
+
13
+ ## [4.0.15](https://github.com/tamylaa/clodo-framework/compare/v4.0.14...v4.0.15) (2026-01-31)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * **lint:** remove unnecessary escape in semver regex ([72751bd](https://github.com/tamylaa/clodo-framework/commit/72751bdf9a98b2f6b1f97706b175ea31f3aa0df1))
19
+
1
20
  ## [4.0.14](https://github.com/tamylaa/clodo-framework/compare/v4.0.13...v4.0.14) (2026-01-19)
2
21
 
3
22
 
@@ -0,0 +1,22 @@
1
+ // Centralized configuration for service payload schema values
2
+ // Allows runtime overrides for enums and features to keep validation flexible and configurable
3
+
4
+ let config = {
5
+ serviceTypes: ['api-service', 'data-service', 'worker', 'pages', 'gateway', 'generic'],
6
+ features: ['d1', 'upstash', 'r2', 'pages', 'ws', 'durableObject', 'cron', 'metrics']
7
+ };
8
+ export function getConfig() {
9
+ return {
10
+ ...config
11
+ };
12
+ }
13
+ export function setConfig(updates = {}) {
14
+ if (updates.serviceTypes) config.serviceTypes = [...updates.serviceTypes];
15
+ if (updates.features) config.features = [...updates.features];
16
+ }
17
+ export function resetConfig() {
18
+ config = {
19
+ serviceTypes: ['api-service', 'data-service', 'worker', 'pages', 'gateway', 'generic'],
20
+ features: ['d1', 'upstash', 'r2', 'pages', 'ws', 'durableObject', 'cron', 'metrics']
21
+ };
22
+ }
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ export * from './schema/SchemaManager.js';
21
21
  export * from './modules/ModuleManager.js';
22
22
  export * from './routing/EnhancedRouter.js';
23
23
  export * from './handlers/GenericRouteHandler.js';
24
+ export { ServiceClient } from './services/ServiceClient.js';
24
25
 
25
26
  // Deployment components
26
27
  export { DeploymentValidator } from './deployment/validator.js';
@@ -32,6 +33,7 @@ export { DeploymentAuditor } from './deployment/auditor.js';
32
33
  export { SecurityCLI } from './security/SecurityCLI.js';
33
34
  export { ConfigurationValidator } from './security/ConfigurationValidator.js';
34
35
  export { SecretGenerator } from './security/SecretGenerator.js';
36
+ export { EnvironmentValidator } from './utils/EnvironmentValidator.js';
35
37
 
36
38
  // Service management components
37
39
  export { ServiceCreator } from './service-management/ServiceCreator.js';
@@ -48,6 +50,9 @@ export { verifyWorkerDeployment, healthCheckWithBackoff, checkHealth } from './l
48
50
  export { classifyError, getRecoverySuggestions } from './lib/shared/error-handling/error-classifier.js';
49
51
 
50
52
  // Framework version info
53
+ export { FrameworkInfo } from './version/FrameworkInfo.js';
54
+ export { TemplateRuntime } from './utils/TemplateRuntime.js';
55
+ export { HealthChecker } from './monitoring/HealthChecker.js';
51
56
  export const FRAMEWORK_VERSION = '1.0.0';
52
57
  export const FRAMEWORK_NAME = 'Clodo Framework';
53
58
 
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Runtime Health Checker
3
+ * Provides health validation and monitoring for services
4
+ */
5
+
6
+ /**
7
+ * Runtime Health Checker for service monitoring
8
+ */
9
+ export class HealthChecker {
10
+ constructor(options = {}) {
11
+ this.options = {
12
+ timeout: options.timeout || 5000,
13
+ interval: options.interval || 30000,
14
+ retries: options.retries || 2,
15
+ ...options
16
+ };
17
+ this.checks = new Map();
18
+ this.lastCheck = null;
19
+ this.overallStatus = 'unknown';
20
+ }
21
+
22
+ /**
23
+ * Add a health check
24
+ * @param {string} name - Check name
25
+ * @param {Function} checkFunction - Async function that returns health status
26
+ * @param {Object} options - Check options
27
+ */
28
+ addCheck(name, checkFunction, options = {}) {
29
+ this.checks.set(name, {
30
+ name,
31
+ checkFunction,
32
+ options: {
33
+ timeout: options.timeout || this.options.timeout,
34
+ critical: options.critical !== false,
35
+ ...options
36
+ },
37
+ lastResult: null,
38
+ lastCheck: null
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Remove a health check
44
+ * @param {string} name - Check name
45
+ */
46
+ removeCheck(name) {
47
+ this.checks.delete(name);
48
+ }
49
+
50
+ /**
51
+ * Run all health checks
52
+ * @returns {Promise<Object>} Health status
53
+ */
54
+ async runChecks() {
55
+ const results = {
56
+ status: 'healthy',
57
+ timestamp: new Date().toISOString(),
58
+ checks: {},
59
+ summary: {
60
+ total: this.checks.size,
61
+ healthy: 0,
62
+ unhealthy: 0,
63
+ unknown: 0
64
+ }
65
+ };
66
+ const checkPromises = Array.from(this.checks.entries()).map(async ([name, check]) => {
67
+ try {
68
+ const startTime = Date.now();
69
+ const result = await this.runSingleCheck(check);
70
+ const duration = Date.now() - startTime;
71
+ results.checks[name] = {
72
+ status: result.status,
73
+ duration,
74
+ timestamp: new Date().toISOString(),
75
+ ...result.details
76
+ };
77
+
78
+ // Update summary
79
+ results.summary[result.status === 'healthy' ? 'healthy' : 'unhealthy']++;
80
+
81
+ // Update overall status
82
+ if (result.status !== 'healthy' && check.options.critical) {
83
+ results.status = 'unhealthy';
84
+ }
85
+ } catch (error) {
86
+ results.checks[name] = {
87
+ status: 'unhealthy',
88
+ error: error.message,
89
+ timestamp: new Date().toISOString()
90
+ };
91
+ results.summary.unhealthy++;
92
+ if (check.options.critical) {
93
+ results.status = 'unhealthy';
94
+ }
95
+ }
96
+ });
97
+ await Promise.all(checkPromises);
98
+ this.lastCheck = results;
99
+ return results;
100
+ }
101
+
102
+ /**
103
+ * Run a single health check with timeout and retries
104
+ * @param {Object} check - Check configuration
105
+ * @returns {Promise<Object>} Check result
106
+ */
107
+ async runSingleCheck(check) {
108
+ let lastError;
109
+ for (let attempt = 0; attempt <= this.options.retries; attempt++) {
110
+ try {
111
+ const controller = new AbortController();
112
+ const timeoutId = setTimeout(() => controller.abort(), check.options.timeout);
113
+ const result = await check.checkFunction({
114
+ signal: controller.signal
115
+ });
116
+ clearTimeout(timeoutId);
117
+ check.lastResult = result;
118
+ check.lastCheck = new Date();
119
+ return {
120
+ status: 'healthy',
121
+ details: result
122
+ };
123
+ } catch (error) {
124
+ lastError = error;
125
+ if (attempt === this.options.retries) {
126
+ break;
127
+ }
128
+
129
+ // Wait before retry
130
+ await this.sleep(1000 * attempt);
131
+ }
132
+ }
133
+ throw lastError;
134
+ }
135
+
136
+ /**
137
+ * Get the last health check results
138
+ * @returns {Object|null} Last check results
139
+ */
140
+ getLastResults() {
141
+ return this.lastCheck;
142
+ }
143
+
144
+ /**
145
+ * Get overall health status
146
+ * @returns {string} Health status
147
+ */
148
+ getStatus() {
149
+ return this.lastCheck?.status || 'unknown';
150
+ }
151
+
152
+ /**
153
+ * Check if service is healthy
154
+ * @returns {boolean} True if healthy
155
+ */
156
+ isHealthy() {
157
+ return this.getStatus() === 'healthy';
158
+ }
159
+
160
+ /**
161
+ * Add common health checks
162
+ * @param {Object} env - Cloudflare Worker environment
163
+ */
164
+ addCommonChecks(env) {
165
+ // Database connectivity check
166
+ if (env.DATABASE) {
167
+ this.addCheck('database', async () => {
168
+ await env.DATABASE.prepare('SELECT 1').first();
169
+ return {
170
+ message: 'Database connection successful'
171
+ };
172
+ });
173
+ }
174
+
175
+ // KV connectivity check
176
+ if (env.KV_CACHE) {
177
+ this.addCheck('kv', async () => {
178
+ await env.KV_CACHE.put('health-check', 'ok', {
179
+ expirationTtl: 60
180
+ });
181
+ return {
182
+ message: 'KV connection successful'
183
+ };
184
+ });
185
+ }
186
+
187
+ // R2 connectivity check
188
+ if (env.R2_STORAGE) {
189
+ this.addCheck('r2', async () => {
190
+ // Try to list objects (will work if bucket exists and is accessible)
191
+ try {
192
+ const objects = await env.R2_STORAGE.list({
193
+ limit: 1
194
+ });
195
+ return {
196
+ message: 'R2 connection successful'
197
+ };
198
+ } catch (error) {
199
+ // If list fails, try a simple head request on a test object
200
+ await env.R2_STORAGE.head('health-check-test');
201
+ return {
202
+ message: 'R2 connection successful'
203
+ };
204
+ }
205
+ });
206
+ }
207
+
208
+ // Memory usage check
209
+ this.addCheck('memory', async () => {
210
+ // In Cloudflare Workers, memory is managed automatically
211
+ // This is just a placeholder for custom memory checks
212
+ return {
213
+ message: 'Memory check passed'
214
+ };
215
+ }, {
216
+ critical: false
217
+ });
218
+ }
219
+
220
+ /**
221
+ * Start periodic health checking
222
+ * @param {Object} env - Cloudflare Worker environment
223
+ */
224
+ startPeriodicChecks(env) {
225
+ // Add common checks if not already added
226
+ if (this.checks.size === 0) {
227
+ this.addCommonChecks(env);
228
+ }
229
+
230
+ // Run initial check
231
+ this.runChecks().catch(error => {
232
+ console.error('Initial health check failed:', error);
233
+ });
234
+
235
+ // Set up periodic checks
236
+ if (this.options.interval > 0) {
237
+ this.intervalId = setInterval(async () => {
238
+ try {
239
+ await this.runChecks();
240
+ } catch (error) {
241
+ console.error('Periodic health check failed:', error);
242
+ }
243
+ }, this.options.interval);
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Stop periodic health checking
249
+ */
250
+ stopPeriodicChecks() {
251
+ if (this.intervalId) {
252
+ clearInterval(this.intervalId);
253
+ this.intervalId = null;
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Get health check summary
259
+ * @returns {Object} Summary of health status
260
+ */
261
+ getSummary() {
262
+ if (!this.lastCheck) {
263
+ return {
264
+ status: 'unknown',
265
+ message: 'No health checks run yet'
266
+ };
267
+ }
268
+ const summary = {
269
+ status: this.lastCheck.status,
270
+ timestamp: this.lastCheck.timestamp,
271
+ totalChecks: this.lastCheck.summary.total,
272
+ healthyChecks: this.lastCheck.summary.healthy,
273
+ unhealthyChecks: this.lastCheck.summary.unhealthy
274
+ };
275
+ return summary;
276
+ }
277
+
278
+ /**
279
+ * Sleep utility
280
+ * @param {number} ms - Milliseconds to sleep
281
+ * @returns {Promise<void>}
282
+ */
283
+ sleep(ms) {
284
+ return new Promise(resolve => setTimeout(resolve, ms));
285
+ }
286
+ }
@@ -11,8 +11,11 @@ export class StateManager {
11
11
  constructor(options = {}) {
12
12
  this.environment = options.environment || 'production';
13
13
  this.dryRun = options.dryRun || false;
14
- this.logDirectory = options.logDirectory || 'deployments';
15
- this.enablePersistence = options.enablePersistence !== false;
14
+ // Default to ephemeral cache directory to avoid polluting workspace root with artifacts
15
+ this.logDirectory = options.logDirectory || '.clodo-cache/deployments';
16
+ // Persistence must be explicitly enabled via options or environment variable to avoid
17
+ // accidentally creating deployment artifacts in developer workspaces.
18
+ this.enablePersistence = options.enablePersistence ?? process.env.CLODO_ENABLE_PERSISTENCE === '1';
16
19
  this.rollbackEnabled = options.rollbackEnabled !== false;
17
20
 
18
21
  // Initialize portfolio state
@@ -258,7 +261,12 @@ export class StateManager {
258
261
  * @returns {Promise<void>}
259
262
  */
260
263
  async saveAuditLog() {
261
- if (!this.enablePersistence) return;
264
+ // Respect persistence opt-in: do not write orchestration artifacts unless enabled
265
+ if (!this.enablePersistence) {
266
+ // Avoid creating files during tests or normal developer runs
267
+ if (this.verbose) console.log(' ℹ️ Persistence disabled: skipping saveAuditLog');
268
+ return;
269
+ }
262
270
  try {
263
271
  // Ensure log directory exists
264
272
  try {
@@ -0,0 +1,9 @@
1
+ import { ServiceOrchestrator } from '../service-management/ServiceOrchestrator.js';
2
+ export async function createServiceProgrammatic(payload, options = {}) {
3
+ const orchestrator = new ServiceOrchestrator({
4
+ interactive: false,
5
+ outputPath: options.outputDir || '.'
6
+ });
7
+ return await orchestrator.createService(payload, options);
8
+ }
9
+ export const createService = createServiceProgrammatic; // Backwards-compatible alias
@@ -460,6 +460,222 @@ export class ConfigurationValidator {
460
460
  return config;
461
461
  }
462
462
 
463
+ /**
464
+ * Validate service configuration consistency between manifest and wrangler config
465
+ * @param {string} manifestPath - Path to manifest.json
466
+ * @param {string} wranglerPath - Path to wrangler.toml
467
+ * @returns {Object} Validation result with issues array
468
+ */
469
+ static validateServiceConfig(manifestPath, wranglerPath) {
470
+ const issues = [];
471
+ try {
472
+ // Read manifest.json
473
+ const manifestContent = readFileSync(manifestPath, 'utf8');
474
+ const manifest = JSON.parse(manifestContent);
475
+
476
+ // Read wrangler.toml (basic parsing - could be enhanced with a TOML parser)
477
+ const wranglerContent = readFileSync(wranglerPath, 'utf8');
478
+ const wranglerConfig = this.parseWranglerToml(wranglerContent);
479
+
480
+ // Validate D1 database consistency
481
+ this.validateD1Consistency(manifest, wranglerConfig, issues);
482
+
483
+ // Validate KV namespace consistency
484
+ this.validateKVConsistency(manifest, wranglerConfig, issues);
485
+
486
+ // Validate R2 bucket consistency
487
+ this.validateR2Consistency(manifest, wranglerConfig, issues);
488
+
489
+ // Validate environment variables
490
+ this.validateEnvironmentConsistency(manifest, wranglerConfig, issues);
491
+ } catch (error) {
492
+ issues.push({
493
+ type: 'error',
494
+ message: `Configuration validation failed: ${error.message}`,
495
+ severity: 'critical'
496
+ });
497
+ }
498
+ return {
499
+ valid: issues.length === 0,
500
+ issues: issues
501
+ };
502
+ }
503
+
504
+ /**
505
+ * Parse wrangler.toml content (basic implementation)
506
+ * @param {string} content - TOML content
507
+ * @returns {Object} Parsed configuration
508
+ */
509
+ static parseWranglerToml(content) {
510
+ const config = {
511
+ d1_databases: [],
512
+ kv_namespaces: [],
513
+ r2_buckets: [],
514
+ vars: {}
515
+ };
516
+ if (!content || !content.trim()) {
517
+ return config;
518
+ }
519
+ const lines = content.split('\n');
520
+ let currentSection = null;
521
+ for (const line of lines) {
522
+ const trimmed = line.trim();
523
+
524
+ // Section headers
525
+ if (trimmed.startsWith('[[')) {
526
+ if (trimmed.includes('d1_databases')) {
527
+ currentSection = 'd1_databases';
528
+ config.d1_databases.push({});
529
+ } else if (trimmed.includes('kv_namespaces')) {
530
+ currentSection = 'kv_namespaces';
531
+ config.kv_namespaces.push({});
532
+ } else if (trimmed.includes('r2_buckets')) {
533
+ currentSection = 'r2_buckets';
534
+ config.r2_buckets.push({});
535
+ }
536
+ } else if (trimmed.startsWith('[') && !trimmed.startsWith('[[')) {
537
+ currentSection = trimmed.replace(/\[|\]/g, '');
538
+ // Support both table and array-of-tables syntaxes by creating a container
539
+ if (Array.isArray(config[currentSection]) && config[currentSection].length === 0) {
540
+ config[currentSection].push({});
541
+ }
542
+ }
543
+ // Key-value pairs
544
+ else if (trimmed.includes('=') && currentSection) {
545
+ const [key, ...valueParts] = trimmed.split('=');
546
+ const value = valueParts.join('=').trim().replace(/"/g, '');
547
+ if (currentSection === 'vars') {
548
+ config.vars[key.trim()] = value;
549
+ } else if (Array.isArray(config[currentSection])) {
550
+ const currentItem = config[currentSection][config[currentSection].length - 1];
551
+ if (currentItem) {
552
+ currentItem[key.trim()] = value;
553
+ }
554
+ }
555
+ }
556
+ }
557
+ return config;
558
+ }
559
+
560
+ /**
561
+ * Validate D1 database consistency
562
+ */
563
+ static validateD1Consistency(manifest, wranglerConfig, issues) {
564
+ const manifestD1 = manifest.d1 || false;
565
+ const wranglerD1 = wranglerConfig.d1_databases || [];
566
+ if (manifestD1 === true && wranglerD1.length === 0) {
567
+ issues.push({
568
+ type: 'mismatch',
569
+ message: 'Manifest declares D1=true but wrangler.toml has no [[d1_databases]] configured',
570
+ severity: 'critical',
571
+ manifest: {
572
+ d1: manifestD1
573
+ },
574
+ wrangler: {
575
+ d1_databases: wranglerD1
576
+ }
577
+ });
578
+ } else if (manifestD1 === false && wranglerD1.length > 0) {
579
+ issues.push({
580
+ type: 'mismatch',
581
+ message: 'Manifest declares D1=false but wrangler.toml has [[d1_databases]] configured',
582
+ severity: 'critical',
583
+ manifest: {
584
+ d1: manifestD1
585
+ },
586
+ wrangler: {
587
+ d1_databases: wranglerD1
588
+ }
589
+ });
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Validate KV namespace consistency
595
+ */
596
+ static validateKVConsistency(manifest, wranglerConfig, issues) {
597
+ const manifestKV = manifest.kv || false;
598
+ const wranglerKV = wranglerConfig.kv_namespaces || [];
599
+ if (manifestKV === true && wranglerKV.length === 0) {
600
+ issues.push({
601
+ type: 'mismatch',
602
+ message: 'Manifest declares KV=true but wrangler.toml has no [[kv_namespaces]] configured',
603
+ severity: 'critical',
604
+ manifest: {
605
+ kv: manifestKV
606
+ },
607
+ wrangler: {
608
+ kv_namespaces: wranglerKV
609
+ }
610
+ });
611
+ } else if (manifestKV === false && wranglerKV.length > 0) {
612
+ issues.push({
613
+ type: 'mismatch',
614
+ message: 'Manifest declares KV=false but wrangler.toml has [[kv_namespaces]] configured',
615
+ severity: 'critical',
616
+ manifest: {
617
+ kv: manifestKV
618
+ },
619
+ wrangler: {
620
+ kv_namespaces: wranglerKV
621
+ }
622
+ });
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Validate R2 bucket consistency
628
+ */
629
+ static validateR2Consistency(manifest, wranglerConfig, issues) {
630
+ const manifestR2 = manifest.r2 || false;
631
+ const wranglerR2 = wranglerConfig.r2_buckets || [];
632
+ if (manifestR2 === true && wranglerR2.length === 0) {
633
+ issues.push({
634
+ type: 'mismatch',
635
+ message: 'Manifest declares R2=true but wrangler.toml has no [[r2_buckets]] configured',
636
+ severity: 'critical',
637
+ manifest: {
638
+ r2: manifestR2
639
+ },
640
+ wrangler: {
641
+ r2_buckets: wranglerR2
642
+ }
643
+ });
644
+ } else if (manifestR2 === false && wranglerR2.length > 0) {
645
+ issues.push({
646
+ type: 'mismatch',
647
+ message: 'Manifest declares R2=false but wrangler.toml has [[r2_buckets]] configured',
648
+ severity: 'critical',
649
+ manifest: {
650
+ r2: manifestR2
651
+ },
652
+ wrangler: {
653
+ r2_buckets: wranglerR2
654
+ }
655
+ });
656
+ }
657
+ }
658
+
659
+ /**
660
+ * Validate environment variable consistency
661
+ */
662
+ static validateEnvironmentConsistency(manifest, wranglerConfig, issues) {
663
+ const manifestEnv = manifest.environment || {};
664
+ const wranglerEnv = wranglerConfig.vars || {};
665
+
666
+ // Check for required environment variables declared in manifest but missing in wrangler
667
+ for (const [key, value] of Object.entries(manifestEnv)) {
668
+ if (value === 'required' && !(key in wranglerEnv)) {
669
+ issues.push({
670
+ type: 'missing_env',
671
+ message: `Manifest declares ${key} as required but not found in wrangler.toml vars`,
672
+ severity: 'warning',
673
+ key: key
674
+ });
675
+ }
676
+ }
677
+ }
678
+
463
679
  /**
464
680
  * Log validation results
465
681
  */
@@ -99,6 +99,54 @@ export class ServiceOrchestrator {
99
99
  }
100
100
  }
101
101
 
102
+ /**
103
+ * Programmatic service creation API
104
+ * Accepts a ServicePayload and performs validation, generation and returns structured result
105
+ */
106
+ async createService(payload, options = {}) {
107
+ // Validate payload using new validator
108
+ const {
109
+ validateServicePayload
110
+ } = await import('../validation/payloadValidation.js');
111
+ const validation = validateServicePayload(payload);
112
+ if (!validation.valid && !options.force) {
113
+ return {
114
+ success: false,
115
+ errors: validation.errors,
116
+ warnings: validation.warnings || []
117
+ };
118
+ }
119
+
120
+ // Map payload to coreInputs expected by generation handlers
121
+ const coreInputs = {
122
+ serviceName: payload.serviceName,
123
+ serviceType: payload.serviceType,
124
+ domainName: payload.domain,
125
+ environment: options.environment || 'development'
126
+ };
127
+ try {
128
+ // Generate derived confirmations and run generation
129
+ const confirmedValues = await this.confirmationHandler.generateAndConfirm(coreInputs);
130
+ const generationResult = await this.generationHandler.generateService(coreInputs, confirmedValues, {
131
+ outputPath: options.outputDir || this.outputPath,
132
+ middlewareStrategy: this.middlewareStrategy
133
+ });
134
+ return {
135
+ success: true,
136
+ serviceId: generationResult.serviceId || null,
137
+ servicePath: generationResult.servicePath || null,
138
+ serviceName: generationResult.serviceName || coreInputs.serviceName,
139
+ fileCount: generationResult.fileCount || (generationResult.generatedFiles || []).length,
140
+ generatedFiles: generationResult.generatedFiles || [],
141
+ warnings: validation.warnings || []
142
+ };
143
+ } catch (error) {
144
+ return {
145
+ success: false,
146
+ errors: [error.message]
147
+ };
148
+ }
149
+ }
102
150
  /**
103
151
  * Validate an existing service configuration
104
152
  */