@tamyla/clodo-framework 4.0.8 ā 4.0.10
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 +7 -0
- package/dist/cli/clodo-service.js +13 -7
- package/dist/cli/commands/init-config.js +39 -4
- package/dist/config/customers.js +0 -2
- package/dist/lib/shared/deployment/validator.js +0 -3
- package/dist/lib/shared/deployment/workflows/interactive-deployment-coordinator.js +2 -6
- package/dist/orchestration/multi-domain-orchestrator.js +70 -13
- package/dist/utils/deployment/wrangler-config-manager.js +0 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [4.0.10](https://github.com/tamylaa/clodo-framework/compare/v4.0.9...v4.0.10) (2025-12-10)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* clean up test artifacts and temporary files ([0eb6cda](https://github.com/tamylaa/clodo-framework/commit/0eb6cdac68bce6d07e567f4b7810f4d94752b7c0))
|
|
7
|
+
|
|
1
8
|
## [4.0.8](https://github.com/tamylaa/clodo-framework/compare/v4.0.7...v4.0.8) (2025-12-09)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
* - list-types List available service types and their features
|
|
17
17
|
*/
|
|
18
18
|
import { Command } from 'commander';
|
|
19
|
+
import { join, dirname } from 'path';
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
19
21
|
import chalk from 'chalk';
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = dirname(__filename);
|
|
20
24
|
|
|
21
25
|
// Create program instance
|
|
22
26
|
const program = new Command();
|
|
@@ -25,33 +29,35 @@ program.name('clodo-service').description('Unified conversational CLI for Clodo
|
|
|
25
29
|
// Dynamically load available command modules
|
|
26
30
|
// This makes the CLI resilient if some commands are excluded from the package
|
|
27
31
|
async function registerAvailableCommands() {
|
|
32
|
+
// Use absolute paths to ensure commands are found regardless of working directory
|
|
33
|
+
const commandsDir = join(__dirname, 'commands');
|
|
28
34
|
const commands = [{
|
|
29
35
|
name: 'create',
|
|
30
|
-
path: '
|
|
36
|
+
path: pathToFileURL(join(commandsDir, 'create.js')).href,
|
|
31
37
|
register: 'registerCreateCommand'
|
|
32
38
|
}, {
|
|
33
39
|
name: 'deploy',
|
|
34
|
-
path: '
|
|
40
|
+
path: pathToFileURL(join(commandsDir, 'deploy.js')).href,
|
|
35
41
|
register: 'registerDeployCommand'
|
|
36
42
|
}, {
|
|
37
43
|
name: 'validate',
|
|
38
|
-
path: '
|
|
44
|
+
path: pathToFileURL(join(commandsDir, 'validate.js')).href,
|
|
39
45
|
register: 'registerValidateCommand'
|
|
40
46
|
}, {
|
|
41
47
|
name: 'update',
|
|
42
|
-
path: '
|
|
48
|
+
path: pathToFileURL(join(commandsDir, 'update.js')).href,
|
|
43
49
|
register: 'registerUpdateCommand'
|
|
44
50
|
}, {
|
|
45
51
|
name: 'diagnose',
|
|
46
|
-
path: '
|
|
52
|
+
path: pathToFileURL(join(commandsDir, 'diagnose.js')).href,
|
|
47
53
|
register: 'registerDiagnoseCommand'
|
|
48
54
|
}, {
|
|
49
55
|
name: 'assess',
|
|
50
|
-
path: '
|
|
56
|
+
path: pathToFileURL(join(commandsDir, 'assess.js')).href,
|
|
51
57
|
register: 'registerAssessCommand'
|
|
52
58
|
}, {
|
|
53
59
|
name: 'init-config',
|
|
54
|
-
path: '
|
|
60
|
+
path: pathToFileURL(join(commandsDir, 'init-config.js')).href,
|
|
55
61
|
register: 'registerInitConfigCommand'
|
|
56
62
|
}];
|
|
57
63
|
for (const cmd of commands) {
|
|
@@ -5,14 +5,46 @@
|
|
|
5
5
|
* Copies the framework's validation-config.json to the service directory for customization
|
|
6
6
|
*/
|
|
7
7
|
import { copyFile, access } from 'fs/promises';
|
|
8
|
-
import { join, dirname } from 'path';
|
|
8
|
+
import { join, dirname, resolve } from 'path';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
12
|
const __dirname = dirname(__filename);
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Find the framework's config directory by walking up from the current working directory
|
|
16
|
+
* or from the command file location
|
|
17
|
+
*/
|
|
18
|
+
export async function findFrameworkConfig() {
|
|
19
|
+
// First try relative to this command file
|
|
20
|
+
const relativeToCommand = join(__dirname, '../../config/validation-config.json');
|
|
21
|
+
try {
|
|
22
|
+
// Check if the file exists at the expected location
|
|
23
|
+
await access(relativeToCommand);
|
|
24
|
+
return relativeToCommand;
|
|
25
|
+
} catch {
|
|
26
|
+
// If not found, try to find it by walking up from current working directory
|
|
27
|
+
let currentDir = process.cwd();
|
|
28
|
+
const maxDepth = 10;
|
|
29
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
30
|
+
const candidatePath = join(currentDir, 'config', 'validation-config.json');
|
|
31
|
+
try {
|
|
32
|
+
await access(candidatePath);
|
|
33
|
+
return candidatePath;
|
|
34
|
+
} catch {
|
|
35
|
+
const parentDir = dirname(currentDir);
|
|
36
|
+
if (parentDir === currentDir) {
|
|
37
|
+
// Reached root directory
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
currentDir = parentDir;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// As fallback, try absolute path from command location
|
|
45
|
+
return resolve(__dirname, '../../config/validation-config.json');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
16
48
|
|
|
17
49
|
/**
|
|
18
50
|
* Register the init-config command with the CLI
|
|
@@ -23,6 +55,9 @@ export function registerInitConfigCommand(program) {
|
|
|
23
55
|
async function handler(options) {
|
|
24
56
|
const targetPath = join(process.cwd(), 'validation-config.json');
|
|
25
57
|
try {
|
|
58
|
+
// Find the framework config file
|
|
59
|
+
const frameworkConfigPath = await findFrameworkConfig();
|
|
60
|
+
|
|
26
61
|
// Check if file already exists
|
|
27
62
|
try {
|
|
28
63
|
await access(targetPath);
|
|
@@ -38,7 +73,7 @@ async function handler(options) {
|
|
|
38
73
|
}
|
|
39
74
|
|
|
40
75
|
// Copy framework config to service directory
|
|
41
|
-
await copyFile(
|
|
76
|
+
await copyFile(frameworkConfigPath, targetPath);
|
|
42
77
|
console.log(chalk.green('ā
Successfully initialized validation-config.json'));
|
|
43
78
|
console.log(chalk.gray('\nš Configuration file details:'));
|
|
44
79
|
console.log(chalk.gray(` Location: ${targetPath}`));
|
package/dist/config/customers.js
CHANGED
|
@@ -38,9 +38,7 @@ export class CustomerConfigurationManager {
|
|
|
38
38
|
const customerDir = resolve(this.configDir, 'customers', customerName);
|
|
39
39
|
|
|
40
40
|
// Create customer directory structure
|
|
41
|
-
console.log(`DEBUG: Checking if directory exists: ${customerDir}`);
|
|
42
41
|
if (!fs.existsSync(customerDir)) {
|
|
43
|
-
console.log(`DEBUG: Creating directory: ${customerDir}`);
|
|
44
42
|
fs.mkdirSync(customerDir, {
|
|
45
43
|
recursive: true
|
|
46
44
|
});
|
|
@@ -598,9 +598,6 @@ export class DeploymentValidator {
|
|
|
598
598
|
}
|
|
599
599
|
async validateDomainEndpoints(domain) {
|
|
600
600
|
console.log(` Validating endpoints for ${domain}...`);
|
|
601
|
-
console.log(` š§ DEBUG: skipEndpointCheck = ${this.options?.skipEndpointCheck}`);
|
|
602
|
-
console.log(` š§ DEBUG: deploymentType = ${this.options?.deploymentType}`);
|
|
603
|
-
console.log(` š§ DEBUG: options = ${JSON.stringify(this.options)}`);
|
|
604
601
|
|
|
605
602
|
// Skip endpoint validation for new deployments
|
|
606
603
|
if (this.options?.skipEndpointCheck) {
|
|
@@ -209,18 +209,14 @@ export class InteractiveDeploymentCoordinator {
|
|
|
209
209
|
async executeDeployment() {
|
|
210
210
|
console.log('š Phase 6: Executing Deployment');
|
|
211
211
|
console.log('ā'.repeat(50));
|
|
212
|
-
const workerName = this.deploymentState.config.worker?.name;
|
|
213
|
-
const databaseName = this.deploymentState.resources.database?.name;
|
|
214
|
-
console.log(` š DEBUG: workerName from state: "${workerName}"`);
|
|
215
|
-
console.log(` š DEBUG: databaseName from state: "${databaseName}"`);
|
|
216
212
|
const result = await Clodo.deploy({
|
|
217
213
|
servicePath: this.deploymentState.config.servicePath,
|
|
218
214
|
environment: this.deploymentState.config.environment,
|
|
219
215
|
domain: this.deploymentState.config.credentials.zoneName,
|
|
220
216
|
serviceName: this.options.serviceName,
|
|
221
|
-
workerName:
|
|
217
|
+
workerName: this.deploymentState.config.worker?.name,
|
|
222
218
|
// Pass the collected worker name
|
|
223
|
-
databaseName:
|
|
219
|
+
databaseName: this.deploymentState.resources.database?.name,
|
|
224
220
|
// Pass the collected database name
|
|
225
221
|
dryRun: this.deploymentState.config.dryRun,
|
|
226
222
|
credentials: this.deploymentState.config.credentials
|
|
@@ -295,8 +295,16 @@ export class MultiDomainOrchestrator {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
/**
|
|
298
|
-
*
|
|
298
|
+
* Get base service name from worker name (remove environment suffixes)
|
|
299
|
+
* @returns {string} Base service name for URL generation
|
|
299
300
|
*/
|
|
301
|
+
getBaseServiceName() {
|
|
302
|
+
if (this.workerName) {
|
|
303
|
+
// Remove environment suffixes from worker name to get base service name
|
|
304
|
+
return this.workerName.replace(/-development$|-staging$|-production$/, '');
|
|
305
|
+
}
|
|
306
|
+
return this.serviceName || 'data-service';
|
|
307
|
+
}
|
|
300
308
|
async setupDomainDatabase(domain) {
|
|
301
309
|
console.log(` šļø Setting up database for ${domain}`);
|
|
302
310
|
if (this.dryRun) {
|
|
@@ -529,7 +537,7 @@ export class MultiDomainOrchestrator {
|
|
|
529
537
|
if (this.dryRun) {
|
|
530
538
|
console.log(` š DRY RUN: Would deploy worker for ${domain}`);
|
|
531
539
|
// Use centralized domain template from validation-config.json
|
|
532
|
-
const customUrl = buildCustomDomain(this.
|
|
540
|
+
const customUrl = buildCustomDomain(this.getBaseServiceName(), domain, this.environment);
|
|
533
541
|
return {
|
|
534
542
|
url: customUrl,
|
|
535
543
|
deployed: false,
|
|
@@ -543,7 +551,6 @@ export class MultiDomainOrchestrator {
|
|
|
543
551
|
// 2. Root wrangler.toml is ephemeral (reflects current active deployment)
|
|
544
552
|
if (this.cloudflareZoneName) {
|
|
545
553
|
console.log(` š§ Preparing customer config for zone: ${this.cloudflareZoneName}`);
|
|
546
|
-
console.log(` š DEBUG: this.workerName is "${this.workerName}"`);
|
|
547
554
|
|
|
548
555
|
// Generate or update customer config with current deployment parameters
|
|
549
556
|
const customerConfigPath = await this.wranglerConfigManager.generateCustomerConfig(this.cloudflareZoneName, {
|
|
@@ -614,7 +621,7 @@ export class MultiDomainOrchestrator {
|
|
|
614
621
|
|
|
615
622
|
// Construct custom domain URL using centralized template from validation-config.json
|
|
616
623
|
// Handles all environment patterns: production, staging, development
|
|
617
|
-
const customUrl = buildCustomDomain(this.
|
|
624
|
+
const customUrl = buildCustomDomain(this.getBaseServiceName(), domain, this.environment);
|
|
618
625
|
|
|
619
626
|
// Store URLs in domain state
|
|
620
627
|
const domainState = this.portfolioState.domainStates.get(domain);
|
|
@@ -626,6 +633,11 @@ export class MultiDomainOrchestrator {
|
|
|
626
633
|
console.log(` ā
Worker deployed successfully`);
|
|
627
634
|
console.log(` š Worker URL: ${workerUrl}`);
|
|
628
635
|
console.log(` š Custom URL: ${customUrl}`);
|
|
636
|
+
|
|
637
|
+
// Display custom domain setup instructions
|
|
638
|
+
if (customUrl && workerUrl !== customUrl) {
|
|
639
|
+
this.displayCustomDomainInstructions(customUrl, workerUrl, domain);
|
|
640
|
+
}
|
|
629
641
|
} else {
|
|
630
642
|
console.log(` ā
Deployment completed (URL not detected in output)`);
|
|
631
643
|
console.log(` š Expected URL: ${customUrl}`);
|
|
@@ -664,14 +676,22 @@ export class MultiDomainOrchestrator {
|
|
|
664
676
|
return true;
|
|
665
677
|
}
|
|
666
678
|
|
|
667
|
-
// Get the deployment URL from domain state
|
|
679
|
+
// Get the deployment URL from domain state - prefer worker URL for immediate validation
|
|
668
680
|
const domainState = this.portfolioState.domainStates.get(domain);
|
|
669
|
-
const
|
|
670
|
-
|
|
681
|
+
const workerUrl = domainState?.workerUrl;
|
|
682
|
+
const customUrl = domainState?.deploymentUrl;
|
|
683
|
+
|
|
684
|
+
// Use worker URL for health check since it's immediately available after deployment
|
|
685
|
+
// Custom domain may require DNS configuration and won't work immediately
|
|
686
|
+
const healthCheckUrl = workerUrl || customUrl;
|
|
687
|
+
if (!healthCheckUrl) {
|
|
671
688
|
console.log(` ā ļø No deployment URL found, skipping health check`);
|
|
672
689
|
return true;
|
|
673
690
|
}
|
|
674
|
-
console.log(` š Running health check: ${
|
|
691
|
+
console.log(` š Running health check: ${healthCheckUrl}/health`);
|
|
692
|
+
if (workerUrl && customUrl && workerUrl !== customUrl) {
|
|
693
|
+
console.log(` ā¹ļø Health check uses worker URL (custom domain requires DNS setup - see below)`);
|
|
694
|
+
}
|
|
675
695
|
|
|
676
696
|
// Retry logic for health checks
|
|
677
697
|
const maxRetries = 3;
|
|
@@ -683,7 +703,7 @@ export class MultiDomainOrchestrator {
|
|
|
683
703
|
console.log(` Attempt ${attempt}/${maxRetries}...`);
|
|
684
704
|
|
|
685
705
|
// Perform actual HTTP health check
|
|
686
|
-
const response = await fetch(`${
|
|
706
|
+
const response = await fetch(`${healthCheckUrl}/health`, {
|
|
687
707
|
method: 'GET',
|
|
688
708
|
headers: {
|
|
689
709
|
'User-Agent': 'Clodo-Orchestrator/2.0'
|
|
@@ -697,7 +717,7 @@ export class MultiDomainOrchestrator {
|
|
|
697
717
|
|
|
698
718
|
// Log successful health check
|
|
699
719
|
this.stateManager.logAuditEvent('HEALTH_CHECK_PASSED', domain, {
|
|
700
|
-
url:
|
|
720
|
+
url: healthCheckUrl,
|
|
701
721
|
status,
|
|
702
722
|
responseTime,
|
|
703
723
|
attempt,
|
|
@@ -708,7 +728,7 @@ export class MultiDomainOrchestrator {
|
|
|
708
728
|
const errorMsg = `Health check returned ${status} - deployment may have issues`;
|
|
709
729
|
console.log(` ā ļø ${errorMsg}`);
|
|
710
730
|
this.stateManager.logAuditEvent('HEALTH_CHECK_WARNING', domain, {
|
|
711
|
-
url:
|
|
731
|
+
url: healthCheckUrl,
|
|
712
732
|
status,
|
|
713
733
|
responseTime,
|
|
714
734
|
attempt,
|
|
@@ -723,9 +743,9 @@ export class MultiDomainOrchestrator {
|
|
|
723
743
|
const errorMsg = `Health check failed: ${error.message}`;
|
|
724
744
|
if (isLastAttempt) {
|
|
725
745
|
console.log(` ā ${errorMsg} (final attempt)`);
|
|
726
|
-
console.log(` š” The service may still be deploying. Check manually: curl ${
|
|
746
|
+
console.log(` š” The service may still be deploying. Check manually: curl ${healthCheckUrl}/health`);
|
|
727
747
|
this.stateManager.logAuditEvent('HEALTH_CHECK_FAILED', domain, {
|
|
728
|
-
url:
|
|
748
|
+
url: healthCheckUrl,
|
|
729
749
|
error: error.message,
|
|
730
750
|
attempts: maxRetries,
|
|
731
751
|
environment: this.environment
|
|
@@ -743,6 +763,43 @@ export class MultiDomainOrchestrator {
|
|
|
743
763
|
return true;
|
|
744
764
|
}
|
|
745
765
|
|
|
766
|
+
/**
|
|
767
|
+
* Display comprehensive custom domain setup instructions
|
|
768
|
+
* @param {string} customUrl - The custom domain URL
|
|
769
|
+
* @param {string} workerUrl - The worker URL to point DNS to
|
|
770
|
+
* @param {string} domain - The domain name
|
|
771
|
+
*/
|
|
772
|
+
displayCustomDomainInstructions(customUrl, workerUrl, domain) {
|
|
773
|
+
console.log(`\nš Custom Domain Setup Instructions`);
|
|
774
|
+
console.log(`ā`.repeat(50));
|
|
775
|
+
console.log(`Your service is deployed and working at: ${workerUrl}`);
|
|
776
|
+
console.log(`To use your custom domain ${customUrl}, follow these steps:\n`);
|
|
777
|
+
console.log(`š Step 1: DNS Configuration`);
|
|
778
|
+
console.log(` Create a CNAME record in your DNS settings:`);
|
|
779
|
+
// Extract subdomain from custom URL (everything before the domain)
|
|
780
|
+
const urlObj = new URL(customUrl);
|
|
781
|
+
const subdomain = urlObj.hostname.replace(`.${domain}`, '');
|
|
782
|
+
console.log(` ⢠Name: ${subdomain}`);
|
|
783
|
+
console.log(` ⢠Type: CNAME`);
|
|
784
|
+
console.log(` ⢠Target: ${workerUrl.replace('https://', '')}`);
|
|
785
|
+
console.log(` ⢠TTL: 300 (5 minutes)\n`);
|
|
786
|
+
console.log(`š§ Step 2: Cloudflare Workers Configuration`);
|
|
787
|
+
console.log(` 1. Go to Cloudflare Dashboard ā Workers & Pages`);
|
|
788
|
+
console.log(` 2. Select your worker`);
|
|
789
|
+
console.log(` 3. Go to "Triggers" tab`);
|
|
790
|
+
console.log(` 4. Click "Add Custom Domain"`);
|
|
791
|
+
console.log(` 5. Enter: ${customUrl}\n`);
|
|
792
|
+
console.log(`ā±ļø Step 3: Wait for Propagation`);
|
|
793
|
+
console.log(` ⢠DNS changes: 5-30 minutes`);
|
|
794
|
+
console.log(` ⢠SSL certificate: 5-10 minutes`);
|
|
795
|
+
console.log(` ⢠Total setup time: 10-60 minutes\n`);
|
|
796
|
+
console.log(`ā
Step 4: Verify Setup`);
|
|
797
|
+
console.log(` Test your custom domain:`);
|
|
798
|
+
console.log(` curl -v ${customUrl}/health\n`);
|
|
799
|
+
console.log(`š” Note: Your service is fully functional at the worker URL immediately.`);
|
|
800
|
+
console.log(` The custom domain is optional for branding/user experience.\n`);
|
|
801
|
+
}
|
|
802
|
+
|
|
746
803
|
/**
|
|
747
804
|
* Get rollback plan using state manager
|
|
748
805
|
* @returns {Array} Rollback plan from state manager
|
|
@@ -136,7 +136,6 @@ export class WranglerConfigManager {
|
|
|
136
136
|
environment = 'production',
|
|
137
137
|
workerName
|
|
138
138
|
} = params;
|
|
139
|
-
console.log(` š DEBUG: generateCustomerConfig called with workerName: ${workerName}`);
|
|
140
139
|
if (!zoneName) {
|
|
141
140
|
throw new Error('Zone name is required to generate customer config');
|
|
142
141
|
}
|
|
@@ -196,7 +195,6 @@ export class WranglerConfigManager {
|
|
|
196
195
|
// Update root-level worker name
|
|
197
196
|
if (config.name) {
|
|
198
197
|
let newWorkerName;
|
|
199
|
-
console.log(` š DEBUG: config.name is "${config.name}", workerName param is "${workerName}"`);
|
|
200
198
|
if (workerName) {
|
|
201
199
|
// Use the provided worker name directly
|
|
202
200
|
newWorkerName = workerName;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamyla/clodo-framework",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.10",
|
|
4
4
|
"description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
"bugs": {
|
|
173
173
|
"url": "https://github.com/tamylaa/clodo-framework/issues"
|
|
174
174
|
},
|
|
175
|
-
"homepage": "https://clodo
|
|
175
|
+
"homepage": "https://clodo.dev",
|
|
176
176
|
"release": {
|
|
177
177
|
"branches": [
|
|
178
178
|
"main",
|