@tamyla/clodo-framework 3.1.12 → 3.1.14
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,18 @@
|
|
|
1
|
+
## [3.1.14](https://github.com/tamylaa/clodo-framework/compare/v3.1.13...v3.1.14) (2025-10-27)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* restore smart credential collection flow in deploy command ([a3e94f6](https://github.com/tamylaa/clodo-framework/commit/a3e94f6efe4a41c380badfc7a104e32bfba9fe9a))
|
|
7
|
+
|
|
8
|
+
## [3.1.13](https://github.com/tamylaa/clodo-framework/compare/v3.1.12...v3.1.13) (2025-10-27)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* add comprehensive test coverage for service validation ([393e6d2](https://github.com/tamylaa/clodo-framework/commit/393e6d268348cc2926c80f0d3f6cd5377c2875a2))
|
|
14
|
+
* add intelligent Cloudflare service detection and validation ([a680006](https://github.com/tamylaa/clodo-framework/commit/a6800063e2df8f8d6fa7660079cd22eb3f9a4a97))
|
|
15
|
+
|
|
1
16
|
## [3.1.12](https://github.com/tamylaa/clodo-framework/compare/v3.1.11...v3.1.12) (2025-10-27)
|
|
2
17
|
|
|
3
18
|
|
|
@@ -2,100 +2,97 @@
|
|
|
2
2
|
* Deploy Command - Smart minimal input deployment with service detection
|
|
3
3
|
*
|
|
4
4
|
* Input Strategy: SMART MINIMAL
|
|
5
|
-
* - Detects
|
|
6
|
-
* -
|
|
5
|
+
* - Detects Clodo services OR legacy services (wrangler.toml)
|
|
6
|
+
* - Supports multiple manifest locations
|
|
7
|
+
* - Gathers credentials smartly: env vars → flags → interactive collection with auto-fetch
|
|
8
|
+
* - Validates token and fetches account ID & zone ID from Cloudflare
|
|
7
9
|
* - Integrates with modular-enterprise-deploy.js for clean CLI-based deployment
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
import chalk from 'chalk';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
+
import { resolve } from 'path';
|
|
14
|
+
import { ManifestLoader } from '../shared/config/manifest-loader.js';
|
|
15
|
+
import { CloudflareServiceValidator } from '../shared/config/cloudflare-service-validator.js';
|
|
16
|
+
import { DeploymentCredentialCollector } from '../shared/deployment/credential-collector.js';
|
|
13
17
|
export function registerDeployCommand(program) {
|
|
14
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 => {
|
|
15
19
|
try {
|
|
16
20
|
console.log(chalk.cyan('\n🚀 Clodo Service Deployment\n'));
|
|
17
21
|
|
|
18
|
-
// Step 1:
|
|
22
|
+
// Step 1: Load and validate service configuration
|
|
19
23
|
const servicePath = resolve(options.servicePath);
|
|
20
|
-
const
|
|
21
|
-
if (!
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const serviceConfig = await ManifestLoader.loadAndValidateCloudflareService(servicePath);
|
|
25
|
+
if (!serviceConfig.manifest) {
|
|
26
|
+
if (serviceConfig.error === 'NOT_A_CLOUDFLARE_SERVICE') {
|
|
27
|
+
ManifestLoader.printNotCloudflareServiceError(servicePath);
|
|
28
|
+
} else if (serviceConfig.error === 'CLOUDFLARE_SERVICE_INVALID') {
|
|
29
|
+
// Pass false because we're validating a detected service, not a Clodo manifest
|
|
30
|
+
ManifestLoader.printValidationErrors(serviceConfig.validationResult, false);
|
|
31
|
+
}
|
|
27
32
|
process.exit(1);
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const manifestContent = readFileSync(manifestPath, 'utf8');
|
|
34
|
-
manifest = JSON.parse(manifestContent);
|
|
35
|
-
} catch (err) {
|
|
36
|
-
console.error(chalk.red('❌ Failed to read service manifest'));
|
|
37
|
-
console.error(chalk.yellow(`Error: ${err.message}`));
|
|
38
|
-
process.exit(1);
|
|
35
|
+
// Print service info and validation results
|
|
36
|
+
if (serviceConfig.validationResult) {
|
|
37
|
+
CloudflareServiceValidator.printValidationReport(serviceConfig.validationResult.validation);
|
|
39
38
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
console.log(chalk.white(`Service: ${chalk.bold(serviceName)}`));
|
|
43
|
-
console.log(chalk.white(`Type: ${serviceType}`));
|
|
44
|
-
console.log(chalk.white(`Path: ${servicePath}\n`));
|
|
45
|
-
|
|
46
|
-
// Step 2: Smart credential gathering
|
|
47
|
-
// Priority: flags → environment variables → fail with helpful message
|
|
48
|
-
const credentials = {
|
|
49
|
-
token: options.token || process.env.CLOUDFLARE_API_TOKEN,
|
|
50
|
-
accountId: options.accountId || process.env.CLOUDFLARE_ACCOUNT_ID,
|
|
51
|
-
zoneId: options.zoneId || process.env.CLOUDFLARE_ZONE_ID
|
|
52
|
-
};
|
|
39
|
+
ManifestLoader.printManifestInfo(serviceConfig.manifest);
|
|
40
|
+
console.log(chalk.gray(`Configuration loaded from: ${serviceConfig.foundAt}\n`));
|
|
53
41
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
42
|
+
// Step 2: Smart credential gathering with interactive collection
|
|
43
|
+
// Uses DeploymentCredentialCollector which:
|
|
44
|
+
// - Checks flags and env vars first
|
|
45
|
+
// - Prompts for API token if needed
|
|
46
|
+
// - Validates token and auto-fetches account ID & zone ID
|
|
47
|
+
// - Caches credentials for future use
|
|
48
|
+
const credentialCollector = new DeploymentCredentialCollector({
|
|
49
|
+
servicePath: servicePath,
|
|
50
|
+
quiet: options.quiet
|
|
51
|
+
});
|
|
52
|
+
let credentials;
|
|
53
|
+
try {
|
|
54
|
+
credentials = await credentialCollector.collectCredentials({
|
|
55
|
+
token: options.token,
|
|
56
|
+
accountId: options.accountId,
|
|
57
|
+
zoneId: options.zoneId
|
|
65
58
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
console.error(chalk.white(' --token <token>'));
|
|
69
|
-
}
|
|
70
|
-
if (missing.includes('CLOUDFLARE_ACCOUNT_ID')) {
|
|
71
|
-
console.error(chalk.white(' --account-id <id>'));
|
|
72
|
-
}
|
|
73
|
-
if (missing.includes('CLOUDFLARE_ZONE_ID')) {
|
|
74
|
-
console.error(chalk.white(' --zone-id <id>'));
|
|
75
|
-
}
|
|
76
|
-
console.error(chalk.cyan('\n Example:'));
|
|
77
|
-
console.error(chalk.white(' npx clodo-service deploy --token abc123 --account-id xyz789 --zone-id def456'));
|
|
78
|
-
console.error(chalk.white(' OR'));
|
|
79
|
-
console.error(chalk.white(' export CLOUDFLARE_API_TOKEN=abc123'));
|
|
80
|
-
console.error(chalk.white(' export CLOUDFLARE_ACCOUNT_ID=xyz789'));
|
|
81
|
-
console.error(chalk.white(' export CLOUDFLARE_ZONE_ID=def456'));
|
|
82
|
-
console.error(chalk.white(' npx clodo-service deploy\n'));
|
|
83
|
-
process.exit(1);
|
|
59
|
+
} finally {
|
|
60
|
+
credentialCollector.cleanup();
|
|
84
61
|
}
|
|
85
62
|
|
|
86
63
|
// Step 3: Extract configuration from manifest
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
64
|
+
const manifest = serviceConfig.manifest;
|
|
65
|
+
const config = manifest.deployment || manifest.configuration || {};
|
|
66
|
+
|
|
67
|
+
// Extract service metadata
|
|
68
|
+
const serviceName = manifest.serviceName || 'unknown-service';
|
|
69
|
+
const serviceType = manifest.serviceType || 'generic';
|
|
70
|
+
|
|
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'));
|
|
93
88
|
}
|
|
94
89
|
console.log(chalk.cyan('📋 Deployment Plan:'));
|
|
95
90
|
console.log(chalk.gray('─'.repeat(50)));
|
|
96
91
|
console.log(chalk.white(`Service: ${serviceName}`));
|
|
97
92
|
console.log(chalk.white(`Type: ${serviceType}`));
|
|
98
|
-
|
|
93
|
+
if (domain) {
|
|
94
|
+
console.log(chalk.white(`Domain: ${domain}`));
|
|
95
|
+
}
|
|
99
96
|
console.log(chalk.white(`Account: ${credentials.accountId.substring(0, 8)}...`));
|
|
100
97
|
console.log(chalk.white(`Zone: ${credentials.zoneId.substring(0, 8)}...`));
|
|
101
98
|
console.log(chalk.gray('─'.repeat(50)));
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Service Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates that a directory is a properly formed Cloudflare Workers service
|
|
5
|
+
* and detects quality issues before deployment attempts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import TOML from '@iarna/toml';
|
|
12
|
+
export class CloudflareServiceValidator {
|
|
13
|
+
/**
|
|
14
|
+
* Check if directory has basic Cloudflare service signatures
|
|
15
|
+
* Returns: { isCloudflareService, wranglerPath, packagePath, errors }
|
|
16
|
+
*/
|
|
17
|
+
static detectCloudflareService(servicePath) {
|
|
18
|
+
const wranglerPath = join(servicePath, 'wrangler.toml');
|
|
19
|
+
const packagePath = join(servicePath, 'package.json');
|
|
20
|
+
const hasWrangler = existsSync(wranglerPath);
|
|
21
|
+
const hasPackage = existsSync(packagePath);
|
|
22
|
+
const errors = [];
|
|
23
|
+
if (!hasWrangler) errors.push('Missing wrangler.toml');
|
|
24
|
+
if (!hasPackage) errors.push('Missing package.json');
|
|
25
|
+
return {
|
|
26
|
+
isCloudflareService: hasWrangler && hasPackage,
|
|
27
|
+
wranglerPath: hasWrangler ? wranglerPath : null,
|
|
28
|
+
packagePath: hasPackage ? packagePath : null,
|
|
29
|
+
errors
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validate service has proper entry points and structure
|
|
35
|
+
* Returns: { isValid, issues, warnings }
|
|
36
|
+
*/
|
|
37
|
+
static validateServiceStructure(servicePath, wranglerPath, packagePath) {
|
|
38
|
+
const issues = [];
|
|
39
|
+
const warnings = [];
|
|
40
|
+
try {
|
|
41
|
+
// Parse files
|
|
42
|
+
const wranglerContent = readFileSync(wranglerPath, 'utf8');
|
|
43
|
+
const wranglerConfig = TOML.parse(wranglerContent);
|
|
44
|
+
const packageContent = readFileSync(packagePath, 'utf8');
|
|
45
|
+
const packageJson = JSON.parse(packageContent);
|
|
46
|
+
|
|
47
|
+
// Check wrangler.toml quality
|
|
48
|
+
if (!wranglerConfig.name) {
|
|
49
|
+
issues.push('wrangler.toml: Missing "name" field');
|
|
50
|
+
}
|
|
51
|
+
if (!wranglerConfig.main) {
|
|
52
|
+
warnings.push('wrangler.toml: No "main" entry point specified (will use default)');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check package.json quality
|
|
56
|
+
if (!packageJson.name) {
|
|
57
|
+
issues.push('package.json: Missing "name" field');
|
|
58
|
+
}
|
|
59
|
+
if (!packageJson.dependencies && !packageJson.devDependencies) {
|
|
60
|
+
warnings.push('package.json: No dependencies defined');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check for common entry points
|
|
64
|
+
const possibleEntryPoints = ['src/worker/index.js', 'src/index.js', 'index.js', 'dist/index.js', wranglerConfig.main].filter(Boolean);
|
|
65
|
+
const hasEntryPoint = possibleEntryPoints.some(ep => existsSync(join(servicePath, ep)));
|
|
66
|
+
if (!hasEntryPoint) {
|
|
67
|
+
warnings.push(`No recognizable entry point found. Checked: ${possibleEntryPoints.join(', ')}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check for Cloudflare environment
|
|
71
|
+
const env = wranglerConfig.env || {};
|
|
72
|
+
if (Object.keys(env).length === 0) {
|
|
73
|
+
warnings.push('wrangler.toml: No environments configured (production, staging, etc)');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Note: Routes are optional - many services define them in code or use catch-all patterns
|
|
77
|
+
// Not warning about missing routes here as it's a valid configuration choice
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
isValid: issues.length === 0,
|
|
81
|
+
issues,
|
|
82
|
+
warnings,
|
|
83
|
+
wranglerConfig,
|
|
84
|
+
packageJson
|
|
85
|
+
};
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return {
|
|
88
|
+
isValid: false,
|
|
89
|
+
issues: [`Failed to parse config files: ${err.message}`],
|
|
90
|
+
warnings: [],
|
|
91
|
+
error: err
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if service has existing deployment history
|
|
98
|
+
*/
|
|
99
|
+
static detectExistingDeployments(servicePath) {
|
|
100
|
+
const possibleLocations = [join(servicePath, '.wrangler'), join(servicePath, 'dist'), join(servicePath, 'build'), join(servicePath, '.deployments')];
|
|
101
|
+
const existingDeployments = possibleLocations.filter(loc => existsSync(loc));
|
|
102
|
+
return {
|
|
103
|
+
hasDeploymentHistory: existingDeployments.length > 0,
|
|
104
|
+
deploymentMarkers: existingDeployments
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Print validation report
|
|
110
|
+
*/
|
|
111
|
+
static printValidationReport(validation) {
|
|
112
|
+
if (validation.issues.length > 0) {
|
|
113
|
+
console.error(chalk.red('\n❌ Service Configuration Issues:\n'));
|
|
114
|
+
validation.issues.forEach(issue => {
|
|
115
|
+
console.error(chalk.red(` • ${issue}`));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (validation.warnings.length > 0) {
|
|
119
|
+
console.warn(chalk.yellow('\n⚠️ Service Configuration Warnings:\n'));
|
|
120
|
+
validation.warnings.forEach(warning => {
|
|
121
|
+
console.warn(chalk.yellow(` • ${warning}`));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (validation.isValid && validation.warnings.length === 0) {
|
|
125
|
+
console.log(chalk.green('\n✅ Service structure looks good!\n'));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Interactive validation: ask user if they want to continue despite warnings
|
|
131
|
+
*/
|
|
132
|
+
static async askContinueDespiteWarnings() {
|
|
133
|
+
// This would be called from interactive context
|
|
134
|
+
// For now, return a promise that can be used in async/await
|
|
135
|
+
return new Promise(resolve => {
|
|
136
|
+
// In actual implementation, would use readline or similar
|
|
137
|
+
// For this stub, we'll indicate the method exists
|
|
138
|
+
resolve(true);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Comprehensive service validation workflow
|
|
144
|
+
*/
|
|
145
|
+
static validateForDeployment(servicePath) {
|
|
146
|
+
// Step 1: Check for basic signatures
|
|
147
|
+
const detection = this.detectCloudflareService(servicePath);
|
|
148
|
+
if (!detection.isCloudflareService) {
|
|
149
|
+
return {
|
|
150
|
+
canDeploy: false,
|
|
151
|
+
reason: 'NOT_A_CLOUDFLARE_SERVICE',
|
|
152
|
+
detection,
|
|
153
|
+
details: 'This directory does not have the required files for a Cloudflare Workers service.'
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Step 2: Validate structure
|
|
158
|
+
const validation = this.validateServiceStructure(servicePath, detection.wranglerPath, detection.packagePath);
|
|
159
|
+
if (!validation.isValid) {
|
|
160
|
+
return {
|
|
161
|
+
canDeploy: false,
|
|
162
|
+
reason: 'SERVICE_CONFIGURATION_INVALID',
|
|
163
|
+
validation,
|
|
164
|
+
details: 'The service configuration is invalid and may not deploy correctly.'
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Step 3: Check deployment history
|
|
169
|
+
const deploymentHistory = this.detectExistingDeployments(servicePath);
|
|
170
|
+
return {
|
|
171
|
+
canDeploy: true,
|
|
172
|
+
reason: validation.warnings.length > 0 ? 'SERVICE_VALID_WITH_WARNINGS' : 'SERVICE_VALID',
|
|
173
|
+
detection,
|
|
174
|
+
validation,
|
|
175
|
+
deploymentHistory,
|
|
176
|
+
details: 'Service is ready for deployment.'
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export default CloudflareServiceValidator;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest Loader - Flexible service manifest detection and loading
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Clodo Framework generated manifests (service-manifest.json)
|
|
6
|
+
* - Legacy services (wrangler.toml + package.json)
|
|
7
|
+
* - Custom manifest locations
|
|
8
|
+
* - Environment variable overrides
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync } from 'fs';
|
|
12
|
+
import { join, resolve } from 'path';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { CloudflareServiceValidator } from './cloudflare-service-validator.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Possible manifest locations (in order of priority)
|
|
18
|
+
*/
|
|
19
|
+
const MANIFEST_LOCATIONS = ['clodo-service-manifest.json',
|
|
20
|
+
// Standard location (root)
|
|
21
|
+
'.clodo/service-manifest.json',
|
|
22
|
+
// Hidden config directory
|
|
23
|
+
'config/service-manifest.json' // Config subdirectory
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fallback configuration builders for legacy services
|
|
28
|
+
*/
|
|
29
|
+
class LegacyServiceDetector {
|
|
30
|
+
/**
|
|
31
|
+
* Build manifest-like config from wrangler.toml + package.json
|
|
32
|
+
*/
|
|
33
|
+
static async buildFromWrangler(servicePath) {
|
|
34
|
+
const wranglerPath = join(servicePath, 'wrangler.toml');
|
|
35
|
+
const packagePath = join(servicePath, 'package.json');
|
|
36
|
+
if (!existsSync(wranglerPath) || !existsSync(packagePath)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
41
|
+
return {
|
|
42
|
+
_source: 'legacy-wrangler',
|
|
43
|
+
_legacyNote: 'This is a legacy service without a service-manifest.json. Consider running: npx clodo-service init',
|
|
44
|
+
serviceName: packageJson.name || 'unknown-service',
|
|
45
|
+
serviceType: 'legacy-workers-project',
|
|
46
|
+
version: packageJson.version || '1.0.0',
|
|
47
|
+
isClodoService: false,
|
|
48
|
+
isLegacyService: true,
|
|
49
|
+
deployment: {
|
|
50
|
+
framework: 'wrangler',
|
|
51
|
+
ready: true,
|
|
52
|
+
configFiles: {
|
|
53
|
+
wrangler: './wrangler.toml',
|
|
54
|
+
package: './package.json'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
metadata: {
|
|
58
|
+
detectedAt: new Date().toISOString(),
|
|
59
|
+
framework: 'legacy-wrangler'
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build manifest-like config from package.json alone
|
|
69
|
+
*/
|
|
70
|
+
static async buildFromPackageJson(servicePath) {
|
|
71
|
+
const packagePath = join(servicePath, 'package.json');
|
|
72
|
+
if (!existsSync(packagePath)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
77
|
+
return {
|
|
78
|
+
_source: 'legacy-package-only',
|
|
79
|
+
_legacyNote: 'This project has a package.json but no deployment configuration.',
|
|
80
|
+
serviceName: packageJson.name || 'unknown-service',
|
|
81
|
+
serviceType: 'nodejs-project',
|
|
82
|
+
version: packageJson.version || '1.0.0',
|
|
83
|
+
isClodoService: false,
|
|
84
|
+
isLegacyService: true,
|
|
85
|
+
deployment: {
|
|
86
|
+
framework: 'nodejs',
|
|
87
|
+
ready: false,
|
|
88
|
+
configFiles: {
|
|
89
|
+
package: './package.json'
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
metadata: {
|
|
93
|
+
detectedAt: new Date().toISOString(),
|
|
94
|
+
framework: 'legacy-nodejs'
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Main manifest loader
|
|
105
|
+
*/
|
|
106
|
+
export class ManifestLoader {
|
|
107
|
+
/**
|
|
108
|
+
* Load service configuration with intelligent Cloudflare service validation
|
|
109
|
+
*
|
|
110
|
+
* Returns:
|
|
111
|
+
* - Clodo Framework manifest (if found)
|
|
112
|
+
* - Validated Cloudflare service config (wrangler.toml + package.json)
|
|
113
|
+
* - Error details if not a valid Cloudflare service
|
|
114
|
+
*/
|
|
115
|
+
static async loadAndValidateCloudflareService(servicePath = '.') {
|
|
116
|
+
const resolvedPath = resolve(servicePath);
|
|
117
|
+
|
|
118
|
+
// Step 1: Try loading Clodo manifest first
|
|
119
|
+
for (const location of MANIFEST_LOCATIONS) {
|
|
120
|
+
const manifestPath = join(resolvedPath, location);
|
|
121
|
+
if (existsSync(manifestPath)) {
|
|
122
|
+
try {
|
|
123
|
+
const content = readFileSync(manifestPath, 'utf8');
|
|
124
|
+
const manifest = JSON.parse(content);
|
|
125
|
+
manifest._source = 'clodo-manifest';
|
|
126
|
+
manifest._location = location;
|
|
127
|
+
manifest.isClodoService = true;
|
|
128
|
+
manifest.isValidCloudflareService = true;
|
|
129
|
+
return {
|
|
130
|
+
manifest,
|
|
131
|
+
foundAt: manifestPath,
|
|
132
|
+
isClodo: true,
|
|
133
|
+
validationResult: null
|
|
134
|
+
};
|
|
135
|
+
} catch (err) {
|
|
136
|
+
throw new Error(`Failed to parse manifest at ${manifestPath}: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Step 2: Validate as Cloudflare service (requires wrangler.toml + package.json)
|
|
142
|
+
const validationResult = CloudflareServiceValidator.validateForDeployment(resolvedPath);
|
|
143
|
+
if (!validationResult.canDeploy && validationResult.reason === 'NOT_A_CLOUDFLARE_SERVICE') {
|
|
144
|
+
return {
|
|
145
|
+
manifest: null,
|
|
146
|
+
foundAt: null,
|
|
147
|
+
isClodo: false,
|
|
148
|
+
validationResult,
|
|
149
|
+
error: 'NOT_A_CLOUDFLARE_SERVICE'
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Step 3: Build manifest-like config from Cloudflare service
|
|
154
|
+
if (validationResult.canDeploy || validationResult.reason === 'SERVICE_VALID_WITH_WARNINGS') {
|
|
155
|
+
const manifest = this.buildFromCloudflareService(validationResult, resolvedPath);
|
|
156
|
+
return {
|
|
157
|
+
manifest,
|
|
158
|
+
foundAt: 'detected-cloudflare-service',
|
|
159
|
+
isClodo: false,
|
|
160
|
+
validationResult,
|
|
161
|
+
isValidCloudflareService: true
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Service is Cloudflare but has issues
|
|
166
|
+
return {
|
|
167
|
+
manifest: null,
|
|
168
|
+
foundAt: null,
|
|
169
|
+
isClodo: false,
|
|
170
|
+
validationResult,
|
|
171
|
+
error: 'CLOUDFLARE_SERVICE_INVALID'
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build manifest-like config from valid Cloudflare service files
|
|
177
|
+
*/
|
|
178
|
+
static buildFromCloudflareService(validationResult, servicePath) {
|
|
179
|
+
const wranglerConfig = validationResult.validation.wranglerConfig;
|
|
180
|
+
const packageJson = validationResult.validation.packageJson;
|
|
181
|
+
return {
|
|
182
|
+
_source: 'cloudflare-service-detected',
|
|
183
|
+
_legacyNote: 'This is a Cloudflare Workers service. Consider creating it with: npx clodo-service create',
|
|
184
|
+
serviceName: packageJson.name || wranglerConfig.name || 'unknown-service',
|
|
185
|
+
serviceType: 'cloudflare-workers-service',
|
|
186
|
+
version: packageJson.version || '1.0.0',
|
|
187
|
+
isClodoService: false,
|
|
188
|
+
isValidCloudflareService: true,
|
|
189
|
+
deployment: {
|
|
190
|
+
framework: 'wrangler',
|
|
191
|
+
ready: validationResult.deploymentHistory.hasDeploymentHistory,
|
|
192
|
+
configFiles: {
|
|
193
|
+
wrangler: './wrangler.toml',
|
|
194
|
+
package: './package.json'
|
|
195
|
+
},
|
|
196
|
+
hasExistingDeployments: validationResult.deploymentHistory.hasDeploymentHistory
|
|
197
|
+
},
|
|
198
|
+
metadata: {
|
|
199
|
+
detectedAt: new Date().toISOString(),
|
|
200
|
+
framework: 'wrangler',
|
|
201
|
+
detectionReason: validationResult.reason
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Print helpful error message for non-Cloudflare services
|
|
208
|
+
*/
|
|
209
|
+
static printNotCloudflareServiceError(servicePath = '.') {
|
|
210
|
+
console.error(chalk.red('\n❌ Not a Cloudflare Workers Service\n'));
|
|
211
|
+
console.error(chalk.white('This directory is missing required Cloudflare files:'));
|
|
212
|
+
console.error(chalk.gray(' - wrangler.toml'));
|
|
213
|
+
console.error(chalk.gray(' - package.json'));
|
|
214
|
+
console.error(chalk.cyan('\n📋 To deploy, you need either:\n'));
|
|
215
|
+
console.error(chalk.white('Option 1: Create a new Clodo service'));
|
|
216
|
+
console.error(chalk.green(' npx clodo-service create'));
|
|
217
|
+
console.error(chalk.gray(' (Creates: service-manifest.json + full structure)\n'));
|
|
218
|
+
console.error(chalk.white('Option 2: Initialize an existing project'));
|
|
219
|
+
console.error(chalk.green(' npx clodo-service init'));
|
|
220
|
+
console.error(chalk.gray(' (Creates: service-manifest.json from existing files)\n'));
|
|
221
|
+
console.error(chalk.white('Option 3: Create a standard Cloudflare project'));
|
|
222
|
+
console.error(chalk.green(' npm init wrangler@latest'));
|
|
223
|
+
console.error(chalk.gray(' (Creates: wrangler.toml + package.json)\n'));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Print validation issues for malformed services
|
|
228
|
+
*/
|
|
229
|
+
static printValidationErrors(validationResult, isClodoValidation = false) {
|
|
230
|
+
if (validationResult.validation.issues.length > 0) {
|
|
231
|
+
console.error(chalk.red('\n❌ Service Configuration Issues:\n'));
|
|
232
|
+
validationResult.validation.issues.forEach(issue => {
|
|
233
|
+
console.error(chalk.red(` • ${issue}`));
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (validationResult.validation.warnings.length > 0) {
|
|
237
|
+
console.warn(chalk.yellow('\n⚠️ Service Configuration Warnings:\n'));
|
|
238
|
+
validationResult.validation.warnings.forEach(warning => {
|
|
239
|
+
console.warn(chalk.yellow(` • ${warning}`));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Different message based on whether this is a Clodo manifest or detected service
|
|
243
|
+
if (isClodoValidation) {
|
|
244
|
+
console.warn(chalk.cyan('\nFor Clodo manifest deployments, review these warnings before proceeding.'));
|
|
245
|
+
} else {
|
|
246
|
+
console.warn(chalk.cyan('\nThese warnings indicate optional configurations that may affect behavior.'));
|
|
247
|
+
console.warn(chalk.cyan('Deployment can proceed, but you may need to handle routing/environments in your worker code.'));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Validate manifest has required fields for deployment
|
|
254
|
+
*/
|
|
255
|
+
static validateDeploymentReady(manifest) {
|
|
256
|
+
const errors = [];
|
|
257
|
+
|
|
258
|
+
// Required for both Clodo and legacy services
|
|
259
|
+
if (!manifest.serviceName) errors.push('Missing: serviceName');
|
|
260
|
+
if (!manifest.deployment) errors.push('Missing: deployment configuration');
|
|
261
|
+
|
|
262
|
+
// For Clodo services
|
|
263
|
+
if (manifest.isClodoService) {
|
|
264
|
+
if (!manifest.deployment.domains || manifest.deployment.domains.length === 0) {
|
|
265
|
+
errors.push('Missing: deployment.domains (required for multi-domain deployment)');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
isValid: errors.length === 0,
|
|
270
|
+
errors
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Print manifest info for debugging
|
|
276
|
+
*/
|
|
277
|
+
static printManifestInfo(manifest) {
|
|
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)}`));
|
|
281
|
+
console.log(chalk.white(`Source: ${chalk.gray(manifest._source)}`));
|
|
282
|
+
if (manifest._legacyNote) {
|
|
283
|
+
console.log(chalk.yellow(`\nℹ️ ${manifest._legacyNote}`));
|
|
284
|
+
}
|
|
285
|
+
if (manifest.deployment.domains) {
|
|
286
|
+
console.log(chalk.white(`\nDomains:`));
|
|
287
|
+
manifest.deployment.domains.forEach(domain => {
|
|
288
|
+
console.log(chalk.gray(` - ${domain.name} (${domain.environment})`));
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
console.log('');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
export default ManifestLoader;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Credential Collector
|
|
3
|
+
*
|
|
4
|
+
* Smart credential collection for deployment:
|
|
5
|
+
* - Priority: flags → env vars → prompt → validate & auto-fetch
|
|
6
|
+
* - Uses ApiTokenManager for secure token storage
|
|
7
|
+
* - Uses CloudflareAPI for token validation and domain/zone discovery
|
|
8
|
+
* - Derives missing credentials from valid token
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { askUser, askPassword, askChoice, closePrompts } from '../utils/interactive-prompts.js';
|
|
13
|
+
import { ApiTokenManager } from '../security/api-token-manager.js';
|
|
14
|
+
import { CloudflareAPI } from "../../../utils/cloudflare/api.js";
|
|
15
|
+
export class DeploymentCredentialCollector {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.servicePath = options.servicePath || '.';
|
|
18
|
+
this.quiet = options.quiet || false;
|
|
19
|
+
this.tokenManager = new ApiTokenManager();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Collect all deployment credentials intelligently
|
|
24
|
+
*
|
|
25
|
+
* Strategy:
|
|
26
|
+
* 1. Check for flags/env vars for all 3 credentials
|
|
27
|
+
* 2. If any missing, prompt for API token first
|
|
28
|
+
* 3. Validate API token and use it to fetch missing credentials
|
|
29
|
+
* 4. Return complete credential set
|
|
30
|
+
*/
|
|
31
|
+
async collectCredentials(options = {}) {
|
|
32
|
+
const startCredentials = {
|
|
33
|
+
token: options.token || process.env.CLOUDFLARE_API_TOKEN || null,
|
|
34
|
+
accountId: options.accountId || process.env.CLOUDFLARE_ACCOUNT_ID || null,
|
|
35
|
+
zoneId: options.zoneId || process.env.CLOUDFLARE_ZONE_ID || null
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// All credentials provided - quick path
|
|
39
|
+
if (startCredentials.token && startCredentials.accountId && startCredentials.zoneId) {
|
|
40
|
+
if (!this.quiet) {
|
|
41
|
+
console.log(chalk.green('\n✅ All credentials provided via flags or environment variables'));
|
|
42
|
+
}
|
|
43
|
+
return startCredentials;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Need to collect interactively
|
|
47
|
+
if (!this.quiet) {
|
|
48
|
+
console.log(chalk.cyan('\n🔐 Deployment Credentials\n'));
|
|
49
|
+
console.log(chalk.white('Clodo uses a smart credential collection strategy:'));
|
|
50
|
+
console.log(chalk.gray('1. Use provided credentials if available'));
|
|
51
|
+
console.log(chalk.gray('2. Prompt for Cloudflare API token'));
|
|
52
|
+
console.log(chalk.gray('3. Auto-fetch account ID and zone ID from token'));
|
|
53
|
+
console.log(chalk.gray('4. Validate and cache credentials\n'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Step 1: Get/prompt for API token
|
|
57
|
+
let token = startCredentials.token;
|
|
58
|
+
if (!token) {
|
|
59
|
+
token = await this.promptForToken();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Step 2: Validate token and get credential options
|
|
63
|
+
if (!this.quiet) {
|
|
64
|
+
console.log(chalk.cyan('\n🔍 Validating Cloudflare API token...\n'));
|
|
65
|
+
}
|
|
66
|
+
let accountId = startCredentials.accountId;
|
|
67
|
+
let zoneId = startCredentials.zoneId;
|
|
68
|
+
try {
|
|
69
|
+
const cloudflareAPI = new CloudflareAPI(token);
|
|
70
|
+
|
|
71
|
+
// Verify token is valid
|
|
72
|
+
const verification = await cloudflareAPI.verifyToken();
|
|
73
|
+
if (!verification.valid) {
|
|
74
|
+
console.error(chalk.red(`\n❌ Invalid Cloudflare API token`));
|
|
75
|
+
console.error(chalk.yellow(verification.error));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
if (!this.quiet) {
|
|
79
|
+
console.log(chalk.green('✅ API token verified\n'));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If account ID not provided, fetch from Cloudflare
|
|
83
|
+
if (!accountId) {
|
|
84
|
+
accountId = await this.fetchAccountId(cloudflareAPI);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If zone ID not provided, fetch from Cloudflare
|
|
88
|
+
if (!zoneId) {
|
|
89
|
+
zoneId = await this.fetchZoneId(cloudflareAPI);
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(chalk.red(`\n❌ Credential validation failed:`));
|
|
93
|
+
console.error(chalk.yellow(error.message));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
if (!this.quiet) {
|
|
97
|
+
console.log(chalk.green('\n✅ All credentials collected and validated\n'));
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
token,
|
|
101
|
+
accountId,
|
|
102
|
+
zoneId
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Prompt user for Cloudflare API token
|
|
108
|
+
*/
|
|
109
|
+
async promptForToken() {
|
|
110
|
+
console.log(chalk.cyan('🔑 Cloudflare API Token\n'));
|
|
111
|
+
console.log(chalk.white('Your API token is used to:'));
|
|
112
|
+
console.log(chalk.gray(' • Verify your Cloudflare account'));
|
|
113
|
+
console.log(chalk.gray(' • Fetch your account ID and domains'));
|
|
114
|
+
console.log(chalk.gray(' • Deploy your service\n'));
|
|
115
|
+
console.log(chalk.cyan('💡 How to get your API token:'));
|
|
116
|
+
console.log(chalk.white(' 1. Go to: https://dash.cloudflare.com/profile/api-tokens'));
|
|
117
|
+
console.log(chalk.white(' 2. Click "Create Token"'));
|
|
118
|
+
console.log(chalk.white(' 3. Use "Edit Cloudflare Workers" template'));
|
|
119
|
+
console.log(chalk.white(' 4. Copy the token and paste it below\n'));
|
|
120
|
+
|
|
121
|
+
// Check if token exists in cache
|
|
122
|
+
if (this.tokenManager.hasToken('cloudflare')) {
|
|
123
|
+
const cached = await askUser('Use cached Cloudflare API token? (yes/no)', 'yes');
|
|
124
|
+
if (cached.toLowerCase() === 'yes' || cached.toLowerCase() === 'y') {
|
|
125
|
+
return this.tokenManager.tokens.cloudflare;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Prompt for new token
|
|
130
|
+
const token = await askPassword('Enter your Cloudflare API token');
|
|
131
|
+
if (!token || token.trim() === '') {
|
|
132
|
+
console.error(chalk.red('❌ API token is required'));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Cache the token for future use
|
|
137
|
+
this.tokenManager.tokens.cloudflare = token.trim();
|
|
138
|
+
this.tokenManager.saveTokens();
|
|
139
|
+
return token.trim();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Fetch account ID from Cloudflare API
|
|
144
|
+
*/
|
|
145
|
+
async fetchAccountId(cloudflareAPI) {
|
|
146
|
+
try {
|
|
147
|
+
if (!this.quiet) {
|
|
148
|
+
console.log(chalk.cyan('📋 Fetching your Cloudflare account ID...\n'));
|
|
149
|
+
}
|
|
150
|
+
const response = await cloudflareAPI.request('/accounts?per_page=100');
|
|
151
|
+
const accounts = response.result || [];
|
|
152
|
+
if (accounts.length === 0) {
|
|
153
|
+
console.error(chalk.red('❌ No Cloudflare accounts found'));
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
if (accounts.length === 1) {
|
|
157
|
+
if (!this.quiet) {
|
|
158
|
+
console.log(chalk.green(`✅ Found account: ${accounts[0].name}\n`));
|
|
159
|
+
}
|
|
160
|
+
return accounts[0].id;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Multiple accounts - let user choose
|
|
164
|
+
if (!this.quiet) {
|
|
165
|
+
console.log(chalk.white('Found multiple accounts:\n'));
|
|
166
|
+
}
|
|
167
|
+
const choices = accounts.map((acc, idx) => `${idx + 1}. ${acc.name} (${acc.id})`);
|
|
168
|
+
const selection = await askChoice('Select account:', choices, 0);
|
|
169
|
+
const selectedIndex = parseInt(selection.split('.')[0]) - 1;
|
|
170
|
+
const selectedAccount = accounts[selectedIndex];
|
|
171
|
+
if (!this.quiet) {
|
|
172
|
+
console.log(chalk.green(`✅ Selected: ${selectedAccount.name}\n`));
|
|
173
|
+
}
|
|
174
|
+
return selectedAccount.id;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error(chalk.red('❌ Failed to fetch account ID:'));
|
|
177
|
+
console.error(chalk.yellow(error.message));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Fetch zone ID from Cloudflare API
|
|
184
|
+
*/
|
|
185
|
+
async fetchZoneId(cloudflareAPI) {
|
|
186
|
+
try {
|
|
187
|
+
if (!this.quiet) {
|
|
188
|
+
console.log(chalk.cyan('🌐 Fetching your Cloudflare domains...\n'));
|
|
189
|
+
}
|
|
190
|
+
const response = await cloudflareAPI.request('/zones?per_page=100');
|
|
191
|
+
const zones = response.result || [];
|
|
192
|
+
if (zones.length === 0) {
|
|
193
|
+
console.error(chalk.red('❌ No Cloudflare domains found'));
|
|
194
|
+
console.error(chalk.yellow('Please add a domain to your Cloudflare account first'));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
if (zones.length === 1) {
|
|
198
|
+
if (!this.quiet) {
|
|
199
|
+
console.log(chalk.green(`✅ Found domain: ${zones[0].name}\n`));
|
|
200
|
+
}
|
|
201
|
+
return zones[0].id;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Multiple zones - let user choose
|
|
205
|
+
if (!this.quiet) {
|
|
206
|
+
console.log(chalk.white('Found multiple domains:\n'));
|
|
207
|
+
}
|
|
208
|
+
const choices = zones.map((zone, idx) => `${idx + 1}. ${zone.name} (Status: ${zone.status})`);
|
|
209
|
+
const selection = await askChoice('Select domain for deployment:', choices, 0);
|
|
210
|
+
const selectedIndex = parseInt(selection.split('.')[0]) - 1;
|
|
211
|
+
const selectedZone = zones[selectedIndex];
|
|
212
|
+
if (!this.quiet) {
|
|
213
|
+
console.log(chalk.green(`✅ Selected: ${selectedZone.name}\n`));
|
|
214
|
+
}
|
|
215
|
+
return selectedZone.id;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error(chalk.red('❌ Failed to fetch domains:'));
|
|
218
|
+
console.error(chalk.yellow(error.message));
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Cleanup resources
|
|
225
|
+
*/
|
|
226
|
+
cleanup() {
|
|
227
|
+
closePrompts();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export default DeploymentCredentialCollector;
|