@tamyla/clodo-framework 3.1.13 → 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,10 @@
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
+
1
8
  ## [3.1.13](https://github.com/tamylaa/clodo-framework/compare/v3.1.12...v3.1.13) (2025-10-27)
2
9
 
3
10
 
@@ -4,7 +4,8 @@
4
4
  * Input Strategy: SMART MINIMAL
5
5
  * - Detects Clodo services OR legacy services (wrangler.toml)
6
6
  * - Supports multiple manifest locations
7
- * - Gathers credentials smartly: env vars → flags → fail with helpful message
7
+ * - Gathers credentials smartly: env vars → flags → interactive collection with auto-fetch
8
+ * - Validates token and fetches account ID & zone ID from Cloudflare
8
9
  * - Integrates with modular-enterprise-deploy.js for clean CLI-based deployment
9
10
  */
10
11
 
@@ -12,6 +13,7 @@ import chalk from 'chalk';
12
13
  import { resolve } from 'path';
13
14
  import { ManifestLoader } from '../shared/config/manifest-loader.js';
14
15
  import { CloudflareServiceValidator } from '../shared/config/cloudflare-service-validator.js';
16
+ import { DeploymentCredentialCollector } from '../shared/deployment/credential-collector.js';
15
17
  export function registerDeployCommand(program) {
16
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 => {
17
19
  try {
@@ -24,7 +26,8 @@ export function registerDeployCommand(program) {
24
26
  if (serviceConfig.error === 'NOT_A_CLOUDFLARE_SERVICE') {
25
27
  ManifestLoader.printNotCloudflareServiceError(servicePath);
26
28
  } else if (serviceConfig.error === 'CLOUDFLARE_SERVICE_INVALID') {
27
- ManifestLoader.printValidationErrors(serviceConfig.validationResult);
29
+ // Pass false because we're validating a detected service, not a Clodo manifest
30
+ ManifestLoader.printValidationErrors(serviceConfig.validationResult, false);
28
31
  }
29
32
  process.exit(1);
30
33
  }
@@ -36,60 +39,60 @@ export function registerDeployCommand(program) {
36
39
  ManifestLoader.printManifestInfo(serviceConfig.manifest);
37
40
  console.log(chalk.gray(`Configuration loaded from: ${serviceConfig.foundAt}\n`));
38
41
 
39
- // Step 2: Smart credential gathering
40
- // Priority: flags → environment variables → fail with helpful message
41
- const credentials = {
42
- token: options.token || process.env.CLOUDFLARE_API_TOKEN,
43
- accountId: options.accountId || process.env.CLOUDFLARE_ACCOUNT_ID,
44
- zoneId: options.zoneId || process.env.CLOUDFLARE_ZONE_ID
45
- };
46
-
47
- // Check for missing credentials
48
- const missing = [];
49
- if (!credentials.token) missing.push('CLOUDFLARE_API_TOKEN');
50
- if (!credentials.accountId) missing.push('CLOUDFLARE_ACCOUNT_ID');
51
- if (!credentials.zoneId) missing.push('CLOUDFLARE_ZONE_ID');
52
- if (missing.length > 0) {
53
- console.error(chalk.red('❌ Missing required Cloudflare credentials\n'));
54
- console.error(chalk.white('Please provide via:'));
55
- console.error(chalk.cyan(' Environment Variables:'));
56
- missing.forEach(key => {
57
- console.error(chalk.white(` export ${key}=<your-${key.toLowerCase()}>`));
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
58
58
  });
59
- console.error(chalk.cyan('\n Command Flags:'));
60
- if (missing.includes('CLOUDFLARE_API_TOKEN')) {
61
- console.error(chalk.white(' --token <token>'));
62
- }
63
- if (missing.includes('CLOUDFLARE_ACCOUNT_ID')) {
64
- console.error(chalk.white(' --account-id <id>'));
65
- }
66
- if (missing.includes('CLOUDFLARE_ZONE_ID')) {
67
- console.error(chalk.white(' --zone-id <id>'));
68
- }
69
- console.error(chalk.cyan('\n Example:'));
70
- console.error(chalk.white(' npx clodo-service deploy --token abc123 --account-id xyz789 --zone-id def456'));
71
- console.error(chalk.white(' OR'));
72
- console.error(chalk.white(' export CLOUDFLARE_API_TOKEN=abc123'));
73
- console.error(chalk.white(' export CLOUDFLARE_ACCOUNT_ID=xyz789'));
74
- console.error(chalk.white(' export CLOUDFLARE_ZONE_ID=def456'));
75
- console.error(chalk.white(' npx clodo-service deploy\n'));
76
- process.exit(1);
59
+ } finally {
60
+ credentialCollector.cleanup();
77
61
  }
78
62
 
79
63
  // Step 3: Extract configuration from manifest
80
64
  const manifest = serviceConfig.manifest;
81
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;
82
74
  const domains = config.domains || [];
83
- if (domains.length === 0) {
84
- console.error(chalk.red('❌ No domains configured in service manifest'));
85
- console.error(chalk.yellow('Add domain configuration to: clodo-service-manifest.json'));
86
- process.exit(1);
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'));
87
88
  }
88
89
  console.log(chalk.cyan('📋 Deployment Plan:'));
89
90
  console.log(chalk.gray('─'.repeat(50)));
90
91
  console.log(chalk.white(`Service: ${serviceName}`));
91
92
  console.log(chalk.white(`Type: ${serviceType}`));
92
- console.log(chalk.white(`Domain: ${domain}`));
93
+ if (domain) {
94
+ console.log(chalk.white(`Domain: ${domain}`));
95
+ }
93
96
  console.log(chalk.white(`Account: ${credentials.accountId.substring(0, 8)}...`));
94
97
  console.log(chalk.white(`Zone: ${credentials.zoneId.substring(0, 8)}...`));
95
98
  console.log(chalk.gray('─'.repeat(50)));
@@ -73,11 +73,9 @@ export class CloudflareServiceValidator {
73
73
  warnings.push('wrangler.toml: No environments configured (production, staging, etc)');
74
74
  }
75
75
 
76
- // Check for routes configuration
77
- const routes = wranglerConfig.routes;
78
- if (!routes) {
79
- warnings.push('wrangler.toml: No routes defined (service may not be accessible)');
80
- }
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
+
81
79
  return {
82
80
  isValid: issues.length === 0,
83
81
  issues,
@@ -226,7 +226,7 @@ export class ManifestLoader {
226
226
  /**
227
227
  * Print validation issues for malformed services
228
228
  */
229
- static printValidationErrors(validationResult) {
229
+ static printValidationErrors(validationResult, isClodoValidation = false) {
230
230
  if (validationResult.validation.issues.length > 0) {
231
231
  console.error(chalk.red('\n❌ Service Configuration Issues:\n'));
232
232
  validationResult.validation.issues.forEach(issue => {
@@ -238,7 +238,14 @@ export class ManifestLoader {
238
238
  validationResult.validation.warnings.forEach(warning => {
239
239
  console.warn(chalk.yellow(` • ${warning}`));
240
240
  });
241
- console.warn(chalk.cyan('\nContinue with deployment? (May fail or behave unexpectedly)'));
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
+ }
242
249
  }
243
250
  }
244
251
 
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "3.1.13",
3
+ "version": "3.1.14",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [