@tamyla/clodo-framework 4.0.8 → 4.0.11

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
+ ## [4.0.11](https://github.com/tamylaa/clodo-framework/compare/v4.0.10...v4.0.11) (2025-12-11)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * correct NPM_TOKEN environment variable name in GitHub Actions workflow ([938c953](https://github.com/tamylaa/clodo-framework/commit/938c953d7e2e9f1a679ee566f1a4c7a070e629e6))
7
+ * update .gitignore to prevent test artifacts from being tracked ([83457d5](https://github.com/tamylaa/clodo-framework/commit/83457d58b1e8d44865a593b2b75e39f4509a75bf))
8
+
9
+ ## [4.0.10](https://github.com/tamylaa/clodo-framework/compare/v4.0.9...v4.0.10) (2025-12-10)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * clean up test artifacts and temporary files ([0eb6cda](https://github.com/tamylaa/clodo-framework/commit/0eb6cdac68bce6d07e567f4b7810f4d94752b7c0))
15
+
1
16
  ## [4.0.8](https://github.com/tamylaa/clodo-framework/compare/v4.0.7...v4.0.8) (2025-12-09)
2
17
 
3
18
 
@@ -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: './commands/create.js',
36
+ path: pathToFileURL(join(commandsDir, 'create.js')).href,
31
37
  register: 'registerCreateCommand'
32
38
  }, {
33
39
  name: 'deploy',
34
- path: './commands/deploy.js',
40
+ path: pathToFileURL(join(commandsDir, 'deploy.js')).href,
35
41
  register: 'registerDeployCommand'
36
42
  }, {
37
43
  name: 'validate',
38
- path: './commands/validate.js',
44
+ path: pathToFileURL(join(commandsDir, 'validate.js')).href,
39
45
  register: 'registerValidateCommand'
40
46
  }, {
41
47
  name: 'update',
42
- path: './commands/update.js',
48
+ path: pathToFileURL(join(commandsDir, 'update.js')).href,
43
49
  register: 'registerUpdateCommand'
44
50
  }, {
45
51
  name: 'diagnose',
46
- path: './commands/diagnose.js',
52
+ path: pathToFileURL(join(commandsDir, 'diagnose.js')).href,
47
53
  register: 'registerDiagnoseCommand'
48
54
  }, {
49
55
  name: 'assess',
50
- path: './commands/assess.js',
56
+ path: pathToFileURL(join(commandsDir, 'assess.js')).href,
51
57
  register: 'registerAssessCommand'
52
58
  }, {
53
59
  name: 'init-config',
54
- path: './commands/init-config.js',
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
- // Path to framework's bundled config
15
- const FRAMEWORK_CONFIG_PATH = join(__dirname, '../../config/validation-config.json');
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(FRAMEWORK_CONFIG_PATH, targetPath);
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}`));
@@ -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: workerName,
217
+ workerName: this.deploymentState.config.worker?.name,
222
218
  // Pass the collected worker name
223
- databaseName: 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
- * Setup domain database using DatabaseOrchestrator
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.serviceName, domain, this.environment);
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.serviceName, domain, this.environment);
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 deploymentUrl = domainState?.deploymentUrl;
670
- if (!deploymentUrl) {
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: ${deploymentUrl}/health`);
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(`${deploymentUrl}/health`, {
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: deploymentUrl,
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: deploymentUrl,
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 ${deploymentUrl}/health`);
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: deploymentUrl,
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.8",
3
+ "version": "4.0.11",
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-framework.tamyla.com",
175
+ "homepage": "https://clodo.dev",
176
176
  "release": {
177
177
  "branches": [
178
178
  "main",