@tamyla/clodo-framework 3.1.14 → 3.1.16

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.
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Deploy Command - Smart minimal input deployment with service detection
3
3
  *
4
- * Input Strategy: SMART MINIMAL
4
+ * Input Strategy: SMART MINIMAL WITH DOMAIN ROUTING
5
5
  * - Detects Clodo services OR legacy services (wrangler.toml)
6
6
  * - Supports multiple manifest locations
7
+ * - Uses DomainRouter for intelligent domain selection
7
8
  * - Gathers credentials smartly: env vars → flags → interactive collection with auto-fetch
8
9
  * - Validates token and fetches account ID & zone ID from Cloudflare
9
- * - Integrates with modular-enterprise-deploy.js for clean CLI-based deployment
10
+ * - REFACTORED (Task 3.2): Integrates with MultiDomainOrchestrator for full deployment orchestration
10
11
  */
11
12
 
12
13
  import chalk from 'chalk';
@@ -14,13 +15,43 @@ import { resolve } from 'path';
14
15
  import { ManifestLoader } from '../shared/config/manifest-loader.js';
15
16
  import { CloudflareServiceValidator } from '../shared/config/cloudflare-service-validator.js';
16
17
  import { DeploymentCredentialCollector } from '../shared/deployment/credential-collector.js';
18
+ import { StandardOptions } from '../shared/utils/cli-options.js';
19
+ import { ConfigLoader } from '../shared/utils/config-loader.js';
20
+ import { DomainRouter } from '../shared/routing/domain-router.js';
21
+ import { MultiDomainOrchestrator } from "../../orchestration/multi-domain-orchestrator.js";
17
22
  export function registerDeployCommand(program) {
18
- program.command('deploy').description('Deploy a Clodo service with smart credential handling').option('--token <token>', 'Cloudflare API token').option('--account-id <id>', 'Cloudflare account ID').option('--zone-id <id>', 'Cloudflare zone ID').option('--dry-run', 'Simulate deployment without making changes').option('--quiet', 'Quiet mode - minimal output').option('--service-path <path>', 'Path to service directory', '.').action(async options => {
23
+ const command = program.command('deploy').description('Deploy a Clodo service with smart credential handling and domain selection')
24
+ // Cloudflare-specific options
25
+ .option('--token <token>', 'Cloudflare API token').option('--account-id <id>', 'Cloudflare account ID').option('--zone-id <id>', 'Cloudflare zone ID').option('--domain <domain>', 'Specific domain to deploy to (otherwise prompted if multiple exist)').option('--environment <env>', 'Target environment (development, staging, production)', 'production').option('--all-domains', 'Deploy to all configured domains (ignores --domain flag)').option('--dry-run', 'Simulate deployment without making changes').option('--service-path <path>', 'Path to service directory', '.');
26
+
27
+ // Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
28
+ StandardOptions.define(command).action(async options => {
19
29
  try {
20
- console.log(chalk.cyan('\n🚀 Clodo Service Deployment\n'));
30
+ const output = new (await import('../shared/utils/output-formatter.js')).OutputFormatter(options);
31
+ const configLoader = new ConfigLoader({
32
+ verbose: options.verbose,
33
+ quiet: options.quiet,
34
+ json: options.json
35
+ });
36
+
37
+ // Load config from file if specified
38
+ let configFileData = {};
39
+ if (options.configFile) {
40
+ configFileData = configLoader.loadSafe(options.configFile, {});
41
+ if (options.verbose && !options.quiet) {
42
+ output.info(`Loaded configuration from: ${options.configFile}`);
43
+ }
44
+ }
45
+
46
+ // Substitute environment variables in config
47
+ configFileData = configLoader.substituteEnvironmentVariables(configFileData);
48
+
49
+ // Merge config file defaults with CLI options (CLI takes precedence)
50
+ const mergedOptions = configLoader.merge(configFileData, options);
51
+ output.info('🚀 Clodo Service Deployment');
21
52
 
22
53
  // Step 1: Load and validate service configuration
23
- const servicePath = resolve(options.servicePath);
54
+ const servicePath = resolve(mergedOptions.servicePath || '.');
24
55
  const serviceConfig = await ManifestLoader.loadAndValidateCloudflareService(servicePath);
25
56
  if (!serviceConfig.manifest) {
26
57
  if (serviceConfig.error === 'NOT_A_CLOUDFLARE_SERVICE') {
@@ -37,7 +68,7 @@ export function registerDeployCommand(program) {
37
68
  CloudflareServiceValidator.printValidationReport(serviceConfig.validationResult.validation);
38
69
  }
39
70
  ManifestLoader.printManifestInfo(serviceConfig.manifest);
40
- console.log(chalk.gray(`Configuration loaded from: ${serviceConfig.foundAt}\n`));
71
+ output.info(`Configuration loaded from: ${serviceConfig.foundAt}`);
41
72
 
42
73
  // Step 2: Smart credential gathering with interactive collection
43
74
  // Uses DeploymentCredentialCollector which:
@@ -47,126 +78,246 @@ export function registerDeployCommand(program) {
47
78
  // - Caches credentials for future use
48
79
  const credentialCollector = new DeploymentCredentialCollector({
49
80
  servicePath: servicePath,
50
- quiet: options.quiet
81
+ quiet: mergedOptions.quiet
51
82
  });
52
83
  let credentials;
53
84
  try {
54
85
  credentials = await credentialCollector.collectCredentials({
55
- token: options.token,
56
- accountId: options.accountId,
57
- zoneId: options.zoneId
86
+ token: mergedOptions.token,
87
+ accountId: mergedOptions.accountId,
88
+ zoneId: mergedOptions.zoneId
58
89
  });
59
90
  } finally {
60
91
  credentialCollector.cleanup();
61
92
  }
62
93
 
63
- // Step 3: Extract configuration from manifest
94
+ // Step 3: Initialize DomainRouter for intelligent domain selection
95
+ // REFACTORED (Task 3.2): Use DomainRouter for domain detection and selection
96
+ console.log(chalk.cyan('\n🗺️ Detecting available domains...\n'));
97
+ const router = new DomainRouter({
98
+ environment: mergedOptions.environment || 'production',
99
+ verbose: options.verbose,
100
+ configPath: options.configPath,
101
+ disableOrchestrator: false,
102
+ // We'll use the orchestrator for deployment
103
+ orchestratorOptions: {
104
+ dryRun: options.dryRun || false,
105
+ cloudflareToken: credentials.token,
106
+ cloudflareAccountId: credentials.accountId
107
+ }
108
+ });
109
+
110
+ // Detect domains from manifest
111
+ let detectedDomains = [];
64
112
  const manifest = serviceConfig.manifest;
65
113
  const config = manifest.deployment || manifest.configuration || {};
114
+ const domainsConfig = config.domains || [];
115
+ if (Array.isArray(domainsConfig) && domainsConfig.length > 0) {
116
+ // Handle both array of strings and array of objects
117
+ detectedDomains = domainsConfig.map(d => {
118
+ if (typeof d === 'string') return d;
119
+ if (typeof d === 'object' && d.name) return d.name;
120
+ return null;
121
+ }).filter(d => d !== null);
122
+ }
123
+
124
+ // If no domains in config, try to detect from environment
125
+ if (detectedDomains.length === 0 && manifest._source === 'cloudflare-service-detected') {
126
+ // For detected CF services, use default
127
+ detectedDomains = ['workers.cloudflare.com'];
128
+ }
129
+
130
+ // Domain selection: check CLI flag first, then prompt user
131
+ let selectedDomain = mergedOptions.domain;
132
+ if (!selectedDomain && detectedDomains.length > 0) {
133
+ if (detectedDomains.length === 1) {
134
+ // Only one domain, use it directly
135
+ selectedDomain = detectedDomains[0];
136
+ if (!options.quiet) {
137
+ console.log(chalk.green(`✓ Selected domain: ${selectedDomain}`));
138
+ }
139
+ } else {
140
+ // Multiple domains available - let user choose
141
+ console.log(chalk.cyan('📍 Available domains:'));
142
+ detectedDomains.forEach((d, i) => {
143
+ console.log(chalk.white(` ${i + 1}) ${d}`));
144
+ });
145
+
146
+ // If running interactively, prompt user
147
+ if (process.stdin.isTTY) {
148
+ const {
149
+ createPromptModule
150
+ } = await import('inquirer');
151
+ const prompt = createPromptModule();
152
+ const response = await prompt([{
153
+ type: 'list',
154
+ name: 'selectedDomain',
155
+ message: 'Select domain to deploy to:',
156
+ choices: detectedDomains,
157
+ default: detectedDomains[0]
158
+ }]);
159
+ selectedDomain = response.selectedDomain;
160
+ } else {
161
+ // Non-interactive mode: use first domain
162
+ selectedDomain = detectedDomains[0];
163
+ console.log(chalk.yellow(`⚠️ Non-interactive mode: using first domain: ${selectedDomain}`));
164
+ }
165
+ }
166
+ }
167
+ if (!selectedDomain) {
168
+ if (!options.quiet) {
169
+ console.error(chalk.yellow('⚠️ No domain configured for deployment'));
170
+ console.error(chalk.gray('For Clodo services: add deployment.domains in clodo-service-manifest.json'));
171
+ console.error(chalk.gray('For detected CF services: define routes in wrangler.toml'));
172
+ }
173
+ process.exit(1);
174
+ }
175
+
176
+ // Step 4: Validate domain configuration
177
+ console.log(chalk.cyan('\n🔍 Validating domain configuration...\n'));
178
+ const validation = router.validateConfiguration({
179
+ domains: [selectedDomain],
180
+ environment: mergedOptions.environment || 'production'
181
+ });
182
+ if (!validation.valid) {
183
+ console.error(chalk.red('❌ Configuration validation failed:'));
184
+ validation.errors.forEach(err => {
185
+ console.error(chalk.yellow(` • ${err}`));
186
+ });
187
+ process.exit(1);
188
+ }
189
+ if (validation.warnings && validation.warnings.length > 0) {
190
+ console.log(chalk.yellow('⚠️ Configuration warnings:'));
191
+ validation.warnings.forEach(warn => {
192
+ console.log(chalk.gray(` • ${warn}`));
193
+ });
194
+ }
66
195
 
67
196
  // Extract service metadata
68
197
  const serviceName = manifest.serviceName || 'unknown-service';
69
198
  const serviceType = manifest.serviceType || 'generic';
70
199
 
71
- // For detected Cloudflare services, domain comes from wrangler.toml or environment
72
- // For Clodo services, use first domain if available
73
- let domain = null;
74
- const domains = config.domains || [];
75
- if (domains.length > 0) {
76
- // Clodo service with multiple domains
77
- domain = domains[0].name || domains[0];
78
- } else if (manifest._source === 'cloudflare-service-detected') {
79
- // Detected CF service - get domain from route or config
80
- // For now, use a placeholder since we don't have explicit domain routing in detected services
81
- domain = 'workers.cloudflare.com';
82
- console.log(chalk.gray('Note: Using Cloudflare Workers default domain (add routes in wrangler.toml for custom domains)'));
83
- }
84
- if (!domain && !options.quiet) {
85
- console.error(chalk.yellow('⚠️ No domain configured for deployment'));
86
- console.error(chalk.gray('For Clodo services: add deployment.domains in clodo-service-manifest.json'));
87
- console.error(chalk.gray('For detected CF services: define routes in wrangler.toml'));
88
- }
89
- console.log(chalk.cyan('📋 Deployment Plan:'));
90
- console.log(chalk.gray('─'.repeat(50)));
91
- console.log(chalk.white(`Service: ${serviceName}`));
92
- console.log(chalk.white(`Type: ${serviceType}`));
93
- if (domain) {
94
- console.log(chalk.white(`Domain: ${domain}`));
95
- }
96
- console.log(chalk.white(`Account: ${credentials.accountId.substring(0, 8)}...`));
97
- console.log(chalk.white(`Zone: ${credentials.zoneId.substring(0, 8)}...`));
98
- console.log(chalk.gray('─'.repeat(50)));
200
+ // Display deployment plan
201
+ console.log(chalk.cyan('\n📋 Deployment Plan:'));
202
+ console.log(chalk.gray('─'.repeat(60)));
203
+ console.log(chalk.white(`Service: ${serviceName}`));
204
+ console.log(chalk.white(`Type: ${serviceType}`));
205
+ console.log(chalk.white(`Domain: ${selectedDomain}`));
206
+ console.log(chalk.white(`Environment: ${mergedOptions.environment || 'production'}`));
207
+ console.log(chalk.white(`Account: ${credentials.accountId.substring(0, 8)}...`));
208
+ console.log(chalk.white(`Zone: ${credentials.zoneId.substring(0, 8)}...`));
209
+ console.log(chalk.white(`Deployment Mode: ${options.dryRun ? 'DRY RUN' : 'LIVE'}`));
210
+ console.log(chalk.gray('─'.repeat(60)));
99
211
  if (options.dryRun) {
100
212
  console.log(chalk.yellow('\n🔍 DRY RUN MODE - No changes will be made\n'));
101
213
  }
102
214
 
103
- // Step 4: Load and call modular deployer
104
- console.log(chalk.cyan('\n⚙️ Initializing deployment system...\n'));
105
- let ModularEnterpriseDeployer;
215
+ // Step 5: Initialize MultiDomainOrchestrator for deployment
216
+ // REFACTORED (Task 3.2): Direct orchestrator integration instead of external deployer
217
+ console.log(chalk.cyan('\n⚙️ Initializing orchestration system...\n'));
218
+ const orchestrator = new MultiDomainOrchestrator({
219
+ domains: [selectedDomain],
220
+ environment: mergedOptions.environment || 'production',
221
+ dryRun: options.dryRun || false,
222
+ skipTests: false,
223
+ parallelDeployments: 1,
224
+ // Single domain in this flow
225
+ servicePath: servicePath,
226
+ cloudflareToken: credentials.token,
227
+ cloudflareAccountId: credentials.accountId,
228
+ enablePersistence: !options.dryRun,
229
+ rollbackEnabled: !options.dryRun,
230
+ verbose: options.verbose
231
+ });
106
232
  try {
107
- const module = await import('../../deployment/modular-enterprise-deploy.js');
108
- ModularEnterpriseDeployer = module.ModularEnterpriseDeployer || module.default;
233
+ await orchestrator.initialize();
109
234
  } catch (err) {
110
- console.error(chalk.red('❌ Failed to load deployment system'));
235
+ console.error(chalk.red('❌ Failed to initialize orchestrator'));
111
236
  console.error(chalk.yellow(`Error: ${err.message}`));
112
237
  if (process.env.DEBUG) {
113
238
  console.error(chalk.gray(err.stack));
114
239
  }
115
240
  process.exit(1);
116
241
  }
117
- if (!ModularEnterpriseDeployer) {
118
- console.error(chalk.red('❌ ModularEnterpriseDeployer not found in deployment module'));
119
- process.exit(1);
120
- }
121
242
 
122
- // Create deployer instance with gathered credentials
123
- const deployer = new ModularEnterpriseDeployer({
124
- apiToken: credentials.token,
125
- accountId: credentials.accountId,
126
- zoneId: credentials.zoneId,
127
- domain: domain,
128
- dryRun: options.dryRun,
129
- quiet: options.quiet,
130
- servicePath: servicePath,
131
- serviceName: serviceName,
132
- serviceType: serviceType
133
- });
243
+ // Step 6: Execute deployment via orchestrator
244
+ console.log(chalk.cyan('🚀 Starting deployment via orchestrator...\n'));
245
+ let result;
246
+ try {
247
+ result = await orchestrator.deploySingleDomain(selectedDomain, {
248
+ manifest: manifest,
249
+ credentials: credentials,
250
+ dryRun: options.dryRun,
251
+ environment: mergedOptions.environment || 'production'
252
+ });
253
+ } catch (deployError) {
254
+ console.error(chalk.red('\n❌ Deployment failed during orchestration\n'));
255
+ console.error(chalk.yellow(`Error: ${deployError.message}`));
134
256
 
135
- // Run deployment
136
- console.log(chalk.cyan('🚀 Starting deployment...\n'));
137
- const result = await deployer.run({
138
- manifest: manifest,
139
- credentials: credentials,
140
- dryRun: options.dryRun
141
- });
257
+ // Provide helpful error context
258
+ if (deployError.message.includes('credentials') || deployError.message.includes('auth')) {
259
+ console.error(chalk.yellow('\n💡 Credential Issue:'));
260
+ console.error(chalk.white(' Check your API token, account ID, and zone ID'));
261
+ console.error(chalk.white(' Visit: https://dash.cloudflare.com/profile/api-tokens'));
262
+ }
263
+ if (deployError.message.includes('domain') || deployError.message.includes('zone')) {
264
+ console.error(chalk.yellow('\n� Domain Issue:'));
265
+ console.error(chalk.white(' Verify domain exists in Cloudflare'));
266
+ console.error(chalk.white(' Check API token has zone:read permissions'));
267
+ }
268
+ if (process.env.DEBUG) {
269
+ console.error(chalk.gray('\nFull Stack Trace:'));
270
+ console.error(chalk.gray(deployError.stack));
271
+ }
272
+ process.exit(1);
273
+ }
142
274
 
143
- // Display results
275
+ // Step 7: Display deployment results
144
276
  console.log(chalk.green('\n✅ Deployment Completed Successfully!\n'));
145
- console.log(chalk.gray('─'.repeat(50)));
277
+ console.log(chalk.gray('─'.repeat(60)));
278
+
279
+ // Display results from orchestrator
146
280
  if (result.url) {
147
- console.log(chalk.white(`🌐 Service URL: ${chalk.bold(result.url)}`));
281
+ console.log(chalk.white(`🌐 Service URL: ${chalk.bold(result.url)}`));
148
282
  }
149
- console.log(chalk.white(`📦 Service: ${serviceName}`));
150
- console.log(chalk.white(`🔧 Type: ${serviceType}`));
151
- console.log(chalk.white(`🌍 Domain: ${domain}`));
283
+ console.log(chalk.white(`📦 Service: ${serviceName}`));
284
+ console.log(chalk.white(`🔧 Type: ${serviceType}`));
285
+ console.log(chalk.white(`🌍 Domain: ${selectedDomain}`));
286
+ console.log(chalk.white(`🌎 Environment: ${mergedOptions.environment || 'production'}`));
152
287
  if (result.workerId) {
153
- console.log(chalk.white(`👤 Worker ID: ${result.workerId}`));
288
+ console.log(chalk.white(`👤 Worker ID: ${result.workerId}`));
289
+ }
290
+ if (result.deploymentId) {
291
+ console.log(chalk.white(`📋 Deployment ID: ${result.deploymentId}`));
154
292
  }
155
293
  if (result.status) {
156
294
  const statusColor = result.status.toLowerCase().includes('success') ? chalk.green : chalk.yellow;
157
- console.log(chalk.white(`📊 Status: ${statusColor(result.status)}`));
295
+ console.log(chalk.white(`📊 Status: ${statusColor(result.status)}`));
296
+ }
297
+
298
+ // Display audit information if available
299
+ if (result.auditLog) {
300
+ console.log(chalk.cyan('\n📋 Deployment Audit:'));
301
+ console.log(chalk.gray(` Started: ${result.auditLog.startTime}`));
302
+ console.log(chalk.gray(` Completed: ${result.auditLog.endTime}`));
303
+ console.log(chalk.gray(` Duration: ${result.auditLog.duration}ms`));
158
304
  }
159
- console.log(chalk.gray('─'.repeat(50)));
305
+ console.log(chalk.gray('─'.repeat(60)));
160
306
 
161
- // Next steps
307
+ // Display next steps
162
308
  if (!options.dryRun) {
163
309
  console.log(chalk.cyan('\n💡 Next Steps:'));
164
- console.log(chalk.white(' • Test deployment: curl ' + (result.url || `https://${domain}`)));
165
- console.log(chalk.white(' • View logs: wrangler tail ' + serviceName));
166
- console.log(chalk.white(' • Monitor: https://dash.cloudflare.com'));
310
+ console.log(chalk.white(` • Test deployment: curl ${result.url || `https://${selectedDomain}`}`));
311
+ console.log(chalk.white(` • View logs: wrangler tail ${serviceName}`));
312
+ console.log(chalk.white(` • Monitor: https://dash.cloudflare.com`));
313
+ console.log(chalk.white(` • Check audit logs: See deployment ID above\n`));
314
+ } else {
315
+ console.log(chalk.cyan('\n💡 Dry Run Complete'));
316
+ console.log(chalk.white(` • Review the plan above`));
317
+ console.log(chalk.white(` • Remove --dry-run to execute deployment\n`));
167
318
  }
168
319
  if (process.env.DEBUG && result.details) {
169
- console.log(chalk.gray('\n📋 Full Result:'));
320
+ console.log(chalk.gray('📋 Full Result:'));
170
321
  console.log(chalk.gray(JSON.stringify(result, null, 2)));
171
322
  }
172
323
  } catch (error) {
@@ -176,11 +327,16 @@ export function registerDeployCommand(program) {
176
327
  console.error(chalk.white(' Check your API token, account ID, and zone ID'));
177
328
  console.error(chalk.white(' Visit: https://dash.cloudflare.com/profile/api-tokens'));
178
329
  }
179
- if (error.message.includes('domain')) {
330
+ if (error.message.includes('domain') || error.message.includes('zone')) {
180
331
  console.error(chalk.yellow('\n💡 Domain Issue:'));
181
332
  console.error(chalk.white(' Verify domain exists in Cloudflare'));
182
333
  console.error(chalk.white(' Check API token has zone:read permissions'));
183
334
  }
335
+ if (error.message.includes('orchestration') || error.message.includes('initialization')) {
336
+ console.error(chalk.yellow('\n💡 Orchestration Issue:'));
337
+ console.error(chalk.white(' Check MultiDomainOrchestrator configuration'));
338
+ console.error(chalk.white(' Verify all modular components loaded correctly'));
339
+ }
184
340
  if (process.env.DEBUG) {
185
341
  console.error(chalk.gray('\nFull Stack Trace:'));
186
342
  console.error(chalk.gray(error.stack));
@@ -1,61 +1,73 @@
1
- /**
2
- * Diagnose Command - Diagnose and report issues with an existing service
3
- */
4
-
5
1
  import chalk from 'chalk';
6
2
  import { ServiceOrchestrator } from "../../service-management/ServiceOrchestrator.js";
3
+ import { StandardOptions } from '../shared/utils/cli-options.js';
4
+ import { ConfigLoader } from '../shared/utils/config-loader.js';
7
5
  export function registerDiagnoseCommand(program) {
8
- program.command('diagnose [service-path]').description('Diagnose and report issues with an existing service').option('--deep-scan', 'Perform deep analysis including dependencies and deployment readiness').option('--export-report <file>', 'Export diagnostic report to file').action(async (servicePath, options) => {
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
+
8
+ // Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
9
+ StandardOptions.define(command).action(async (servicePath, options) => {
9
10
  try {
11
+ const output = new (await import('../shared/utils/output-formatter.js')).OutputFormatter(options);
12
+ const configLoader = new ConfigLoader({
13
+ verbose: options.verbose,
14
+ quiet: options.quiet,
15
+ json: options.json
16
+ });
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);
10
29
  const orchestrator = new ServiceOrchestrator();
11
30
 
12
31
  // Auto-detect service path if not provided
13
32
  if (!servicePath) {
14
33
  servicePath = await orchestrator.detectServicePath();
15
34
  if (!servicePath) {
16
- console.error(chalk.red('No service path provided and could not auto-detect service directory'));
35
+ output.error('No service path provided and could not auto-detect service directory');
17
36
  process.exit(1);
18
37
  }
19
38
  }
20
- console.log(chalk.cyan('🔍 Diagnosing service...'));
21
- const diagnosis = await orchestrator.diagnoseService(servicePath, options);
39
+ output.info('🔍 Diagnosing service...');
40
+ const diagnosis = await orchestrator.diagnoseService(servicePath, mergedOptions);
22
41
 
23
42
  // Display results
24
- console.log(chalk.cyan('\n📋 Diagnostic Report'));
25
- console.log(chalk.white(`Service: ${diagnosis.serviceName || 'Unknown'}`));
26
- console.log(chalk.white(`Path: ${servicePath}`));
43
+ output.section('Diagnostic Report');
44
+ output.list([`Service: ${diagnosis.serviceName || 'Unknown'}`, `Path: ${servicePath}`]);
27
45
  if (diagnosis.errors.length > 0) {
28
- console.log(chalk.red('\n❌ Critical Errors:'));
46
+ output.warning('❌ Critical Errors:');
29
47
  diagnosis.errors.forEach(error => {
30
- console.log(chalk.red(` • ${error.message}`));
31
- if (error.location) {
32
- console.log(chalk.gray(` Location: ${error.location}`));
33
- }
34
- if (error.suggestion) {
35
- console.log(chalk.cyan(` 💡 ${error.suggestion}`));
36
- }
48
+ let msg = `• ${error.message}`;
49
+ if (error.location) msg += ` (Location: ${error.location})`;
50
+ if (error.suggestion) msg += ` 💡 ${error.suggestion}`;
51
+ output.warning(msg);
37
52
  });
38
53
  }
39
54
  if (diagnosis.warnings.length > 0) {
40
- console.log(chalk.yellow('\n⚠️ Warnings:'));
55
+ output.warning('⚠️ Warnings:');
41
56
  diagnosis.warnings.forEach(warning => {
42
- console.log(chalk.yellow(` • ${warning.message}`));
43
- if (warning.suggestion) {
44
- console.log(chalk.cyan(` 💡 ${warning.suggestion}`));
45
- }
57
+ let msg = `• ${warning.message}`;
58
+ if (warning.suggestion) msg += ` 💡 ${warning.suggestion}`;
59
+ output.warning(msg);
46
60
  });
47
61
  }
48
62
  if (diagnosis.recommendations.length > 0) {
49
- console.log(chalk.cyan('\n💡 Recommendations:'));
50
- diagnosis.recommendations.forEach(rec => {
51
- console.log(chalk.white(` • ${rec}`));
52
- });
63
+ output.info('💡 Recommendations:');
64
+ output.list(diagnosis.recommendations);
53
65
  }
54
66
 
55
67
  // Export report if requested
56
- if (options.exportReport) {
57
- await orchestrator.exportDiagnosticReport(diagnosis, options.exportReport);
58
- console.log(chalk.green(`\n📄 Report exported to: ${options.exportReport}`));
68
+ if (mergedOptions.exportReport) {
69
+ await orchestrator.exportDiagnosticReport(diagnosis, mergedOptions.exportReport);
70
+ output.success(`📄 Report exported to: ${mergedOptions.exportReport}`);
59
71
  }
60
72
 
61
73
  // Exit with error code if critical issues found
@@ -63,7 +75,8 @@ export function registerDiagnoseCommand(program) {
63
75
  process.exit(1);
64
76
  }
65
77
  } catch (error) {
66
- console.error(chalk.red(`Diagnosis failed: ${error.message}`));
78
+ const output = new (await import('../shared/utils/output-formatter.js')).OutputFormatter(options || {});
79
+ output.error(`Diagnosis failed: ${error.message}`);
67
80
  process.exit(1);
68
81
  }
69
82
  });
@@ -4,50 +4,70 @@
4
4
 
5
5
  import chalk from 'chalk';
6
6
  import { ServiceOrchestrator } from "../../service-management/ServiceOrchestrator.js";
7
+ import { StandardOptions } from '../shared/utils/cli-options.js';
8
+ import { ConfigLoader } from '../shared/utils/config-loader.js';
7
9
  export function registerUpdateCommand(program) {
8
- program.command('update [service-path]').description('Update an existing service configuration').option('-i, --interactive', 'Run in interactive mode to select what to update').option('--domain-name <domain>', 'Update domain name').option('--cloudflare-token <token>', 'Update Cloudflare API token').option('--cloudflare-account-id <id>', 'Update Cloudflare account ID').option('--cloudflare-zone-id <id>', 'Update Cloudflare zone ID').option('--environment <env>', 'Update target environment: development, staging, production').option('--add-feature <feature>', 'Add a feature flag').option('--remove-feature <feature>', 'Remove a feature flag').option('--regenerate-configs', 'Regenerate all configuration files').option('--fix-errors', 'Attempt to fix common configuration errors').action(async (servicePath, options) => {
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
+
12
+ // Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
13
+ StandardOptions.define(command).action(async (servicePath, options) => {
9
14
  try {
15
+ const output = new (await import('../shared/utils/output-formatter.js')).OutputFormatter(options);
16
+ const configLoader = new ConfigLoader({
17
+ verbose: options.verbose,
18
+ quiet: options.quiet,
19
+ json: options.json
20
+ });
21
+
22
+ // Load config from file if specified
23
+ let configFileData = {};
24
+ if (options.configFile) {
25
+ configFileData = configLoader.loadSafe(options.configFile, {});
26
+ if (options.verbose && !options.quiet) {
27
+ output.info(`Loaded configuration from: ${options.configFile}`);
28
+ }
29
+ }
30
+
31
+ // Merge config file defaults with CLI options (CLI takes precedence)
32
+ const mergedOptions = configLoader.merge(configFileData, options);
10
33
  const orchestrator = new ServiceOrchestrator();
11
34
 
12
35
  // Auto-detect service path if not provided
13
36
  if (!servicePath) {
14
37
  servicePath = await orchestrator.detectServicePath();
15
38
  if (!servicePath) {
16
- console.error(chalk.red('No service path provided and could not auto-detect service directory'));
17
- console.log(chalk.white('Please run this command from within a service directory or specify the path'));
39
+ output.error('No service path provided and could not auto-detect service directory');
40
+ output.info('Please run this command from within a service directory or specify the path');
18
41
  process.exit(1);
19
42
  }
20
- console.log(chalk.cyan(`Auto-detected service at: ${servicePath}`));
43
+ output.info(`Auto-detected service at: ${servicePath}`);
21
44
  }
22
45
 
23
46
  // Validate it's a service directory
24
47
  const isValid = await orchestrator.validateService(servicePath);
25
48
  if (!isValid.valid) {
26
- console.log(chalk.yellow('⚠️ Service has configuration issues. Use --fix-errors to attempt automatic fixes.'));
27
- if (!options.fixErrors) {
28
- console.log(chalk.white('Issues found:'));
29
- isValid.issues.forEach(issue => {
30
- console.log(chalk.yellow(` • ${issue}`));
31
- });
49
+ output.warning('Service has configuration issues. Use --fix-errors to attempt automatic fixes.');
50
+ if (!mergedOptions.fixErrors) {
51
+ output.info('Issues found:');
52
+ output.list(isValid.issues || []);
32
53
  process.exit(1);
33
54
  }
34
55
  }
35
- if (options.interactive) {
56
+ if (mergedOptions.interactive) {
36
57
  await orchestrator.runInteractiveUpdate(servicePath);
37
58
  } else {
38
- await orchestrator.runNonInteractiveUpdate(servicePath, options);
59
+ await orchestrator.runNonInteractiveUpdate(servicePath, mergedOptions);
39
60
  }
40
- console.log(chalk.green('\n✓ Service update completed successfully!'));
61
+ output.success('Service update completed successfully!');
41
62
  } catch (error) {
42
- console.error(chalk.red(`\n✗ Service update failed: ${error.message}`));
63
+ const output = new (await import('../shared/utils/output-formatter.js')).OutputFormatter(options || {});
64
+ output.error(`Service update failed: ${error.message}`);
43
65
  if (error.details) {
44
- console.error(chalk.yellow(`Details: ${error.details}`));
66
+ output.warning(`Details: ${error.details}`);
45
67
  }
46
68
  if (error.recovery) {
47
- console.log(chalk.cyan('\n💡 Recovery suggestions:'));
48
- error.recovery.forEach(suggestion => {
49
- console.log(chalk.white(` • ${suggestion}`));
50
- });
69
+ output.info('Recovery suggestions:');
70
+ output.list(error.recovery);
51
71
  }
52
72
  process.exit(1);
53
73
  }