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