@tamyla/clodo-framework 3.0.15 → 3.1.0

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +215 -163
  2. package/README.md +133 -1
  3. package/bin/clodo-service.js +0 -0
  4. package/bin/security/security-cli.js +0 -0
  5. package/bin/service-management/create-service.js +0 -0
  6. package/bin/service-management/init-service.js +2 -1
  7. package/dist/service-management/GenerationEngine.js +298 -3025
  8. package/dist/service-management/ServiceCreator.js +19 -3
  9. package/dist/service-management/generators/BaseGenerator.js +233 -0
  10. package/dist/service-management/generators/GeneratorRegistry.js +254 -0
  11. package/dist/service-management/generators/cicd/CiWorkflowGenerator.js +87 -0
  12. package/dist/service-management/generators/cicd/DeployWorkflowGenerator.js +106 -0
  13. package/dist/service-management/generators/code/ServiceHandlersGenerator.js +235 -0
  14. package/dist/service-management/generators/code/ServiceMiddlewareGenerator.js +116 -0
  15. package/dist/service-management/generators/code/ServiceUtilsGenerator.js +246 -0
  16. package/dist/service-management/generators/code/WorkerIndexGenerator.js +143 -0
  17. package/dist/service-management/generators/config/DevelopmentEnvGenerator.js +101 -0
  18. package/dist/service-management/generators/config/DomainsConfigGenerator.js +175 -0
  19. package/dist/service-management/generators/config/EnvExampleGenerator.js +178 -0
  20. package/dist/service-management/generators/config/ProductionEnvGenerator.js +97 -0
  21. package/dist/service-management/generators/config/StagingEnvGenerator.js +97 -0
  22. package/dist/service-management/generators/config/WranglerTomlGenerator.js +238 -0
  23. package/dist/service-management/generators/core/PackageJsonGenerator.js +243 -0
  24. package/dist/service-management/generators/core/SiteConfigGenerator.js +115 -0
  25. package/dist/service-management/generators/documentation/ApiDocsGenerator.js +331 -0
  26. package/dist/service-management/generators/documentation/ConfigurationDocsGenerator.js +294 -0
  27. package/dist/service-management/generators/documentation/DeploymentDocsGenerator.js +244 -0
  28. package/dist/service-management/generators/documentation/ReadmeGenerator.js +196 -0
  29. package/dist/service-management/generators/schemas/ServiceSchemaGenerator.js +190 -0
  30. package/dist/service-management/generators/scripts/DeployScriptGenerator.js +123 -0
  31. package/dist/service-management/generators/scripts/HealthCheckScriptGenerator.js +101 -0
  32. package/dist/service-management/generators/scripts/SetupScriptGenerator.js +88 -0
  33. package/dist/service-management/generators/service-types/StaticSiteGenerator.js +342 -0
  34. package/dist/service-management/generators/testing/EslintConfigGenerator.js +85 -0
  35. package/dist/service-management/generators/testing/IntegrationTestsGenerator.js +237 -0
  36. package/dist/service-management/generators/testing/JestConfigGenerator.js +72 -0
  37. package/dist/service-management/generators/testing/UnitTestsGenerator.js +277 -0
  38. package/dist/service-management/generators/tooling/DockerComposeGenerator.js +71 -0
  39. package/dist/service-management/generators/tooling/GitignoreGenerator.js +143 -0
  40. package/dist/service-management/generators/utils/FileWriter.js +179 -0
  41. package/dist/service-management/generators/utils/PathResolver.js +157 -0
  42. package/dist/service-management/generators/utils/ServiceManifestGenerator.js +111 -0
  43. package/dist/service-management/generators/utils/TemplateEngine.js +185 -0
  44. package/dist/service-management/generators/utils/index.js +18 -0
  45. package/dist/service-management/routing/DomainRouteMapper.js +311 -0
  46. package/dist/service-management/routing/RouteGenerator.js +266 -0
  47. package/dist/service-management/routing/WranglerRoutesBuilder.js +273 -0
  48. package/dist/service-management/routing/index.js +14 -0
  49. package/dist/service-management/services/DirectoryStructureService.js +56 -0
  50. package/dist/service-management/services/GenerationCoordinator.js +208 -0
  51. package/dist/service-management/services/GeneratorRegistry.js +174 -0
  52. package/dist/services/GenericDataService.js +14 -1
  53. package/dist/utils/config/unified-config-manager.js +128 -12
  54. package/dist/utils/framework-config.js +74 -2
  55. package/dist/worker/integration.js +4 -1
  56. package/package.json +6 -1
  57. package/templates/generic/clodo-service-manifest.json +25 -0
  58. package/templates/static-site/.env.example +61 -0
  59. package/templates/static-site/README.md +176 -0
  60. package/templates/static-site/clodo-service-manifest.json +66 -0
  61. package/templates/static-site/package.json +28 -0
  62. package/templates/static-site/public/404.html +87 -0
  63. package/templates/static-site/public/app.js +100 -0
  64. package/templates/static-site/public/index.html +48 -0
  65. package/templates/static-site/public/styles.css +123 -0
  66. package/templates/static-site/scripts/deploy.ps1 +121 -0
  67. package/templates/static-site/scripts/setup.ps1 +179 -0
  68. package/templates/static-site/src/config/domains.js +35 -0
  69. package/templates/static-site/src/worker/index.js +153 -0
  70. package/templates/static-site/wrangler.toml +43 -0
@@ -6,6 +6,7 @@
6
6
  import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync, readdirSync, statSync } from 'fs';
7
7
  import { join, dirname, resolve } from 'path';
8
8
  import { fileURLToPath } from 'url';
9
+ import { FrameworkConfig } from '../utils/framework-config.js';
9
10
  const SERVICE_TYPES = ['data-service', 'auth-service', 'content-service', 'api-gateway', 'generic'];
10
11
  export class ServiceCreator {
11
12
  constructor(options = {}) {
@@ -19,6 +20,9 @@ export class ServiceCreator {
19
20
  })();
20
21
  this.templatesDir = options.templatesDir || templatesDir;
21
22
  this.serviceTypes = options.serviceTypes || SERVICE_TYPES;
23
+
24
+ // Load framework configuration
25
+ this.frameworkConfig = options.frameworkConfig || new FrameworkConfig();
22
26
  }
23
27
 
24
28
  /**
@@ -74,9 +78,16 @@ export class ServiceCreator {
74
78
  const templateDir = join(this.templatesDir, config.type);
75
79
  const serviceDir = join(config.output, serviceName);
76
80
 
77
- // Check if template exists
81
+ // Check if template exists, fall back to generic if not
82
+ let actualTemplateDir = templateDir;
78
83
  if (!existsSync(templateDir)) {
79
- throw new Error(`Template not found: ${templateDir}. Available templates: ${this.serviceTypes.join(', ')}`);
84
+ const genericTemplate = join(this.templatesDir, 'generic');
85
+ if (existsSync(genericTemplate)) {
86
+ console.log(`⚠️ Template for '${config.type}' not found, using 'generic' template as fallback`);
87
+ actualTemplateDir = genericTemplate;
88
+ } else {
89
+ throw new Error(`Template not found: ${templateDir}. Available templates: ${this.serviceTypes.join(', ')}`);
90
+ }
80
91
  }
81
92
 
82
93
  // Check if service directory already exists
@@ -85,15 +96,20 @@ export class ServiceCreator {
85
96
  }
86
97
 
87
98
  // Copy template to service directory
88
- cpSync(templateDir, serviceDir, {
99
+ cpSync(actualTemplateDir, serviceDir, {
89
100
  recursive: true
90
101
  });
91
102
 
103
+ // Load template defaults from config
104
+ const templateDefaults = this.frameworkConfig.config?.templates?.defaults || {};
105
+
92
106
  // Prepare template variables
93
107
  const defaultVariables = {
94
108
  '{{SERVICE_NAME}}': serviceName,
95
109
  '{{SERVICE_TYPE}}': config.type,
96
110
  '{{SERVICE_DISPLAY_NAME}}': this.toTitleCase(serviceName.replace(/-/g, ' ')),
111
+ '{{DOMAIN_NAME}}': config.domain || templateDefaults.DOMAIN_NAME || 'example.com',
112
+ '{{WORKERS_DEV_DOMAIN}}': templateDefaults.WORKERS_DEV_DOMAIN || 'workers.dev',
97
113
  '{{CURRENT_DATE}}': new Date().toISOString().split('T')[0],
98
114
  '{{CURRENT_YEAR}}': new Date().getFullYear().toString(),
99
115
  '{{FRAMEWORK_VERSION}}': this.getFrameworkVersion()
@@ -0,0 +1,233 @@
1
+ /**
2
+ * BaseGenerator - Abstract base class for all file generators
3
+ *
4
+ * Provides common functionality for loading templates, rendering content,
5
+ * and writing files. All concrete generators should extend this class.
6
+ *
7
+ * NOTE: This class uses Node.js filesystem APIs and is designed for
8
+ * build-time usage during service generation, not runtime in Cloudflare Workers.
9
+ *
10
+ * @abstract
11
+ */
12
+ import { promises as fs } from 'fs';
13
+ import * as path from 'path';
14
+ export class BaseGenerator {
15
+ /**
16
+ * Create a new generator instance
17
+ * @param {Object} options - Generator configuration options
18
+ * @param {string} options.name - Generator name (for logging/debugging)
19
+ * @param {string} options.templatesPath - Path to templates directory
20
+ * @param {string} options.servicePath - Path to service being generated
21
+ */
22
+ constructor(options = {}) {
23
+ if (new.target === BaseGenerator) {
24
+ throw new Error('BaseGenerator is abstract and cannot be instantiated directly');
25
+ }
26
+ this.name = options.name || this.constructor.name;
27
+ this.templatesPath = options.templatesPath || options.templatesDir || null;
28
+ this.servicePath = options.servicePath || null;
29
+
30
+ // Warn if running in Cloudflare Workers environment
31
+ if (typeof globalThis !== 'undefined' && globalThis.caches) {
32
+ console.warn(`⚠️ ${this.name}: Generators are designed for build-time usage, not runtime in Cloudflare Workers`);
33
+ }
34
+ this.context = {};
35
+ this.logger = options.logger || console;
36
+ }
37
+
38
+ /**
39
+ * Main generation method - must be implemented by concrete generators
40
+ * @abstract
41
+ * @param {Object} context - Generation context with service configuration
42
+ * @returns {Promise<void>}
43
+ */
44
+ async generate(context) {
45
+ throw new Error(`generate() must be implemented by ${this.constructor.name}`);
46
+ }
47
+
48
+ /**
49
+ * Determine if this generator should run for the given context
50
+ * Override this to conditionally skip generation
51
+ * @param {Object} context - Generation context
52
+ * @returns {boolean} - True if generator should run
53
+ */
54
+ shouldGenerate(context) {
55
+ return true;
56
+ }
57
+
58
+ /**
59
+ * Set the generation context
60
+ * @param {Object} context - Generation context with service configuration
61
+ */
62
+ setContext(context) {
63
+ this.context = {
64
+ ...context
65
+ };
66
+
67
+ // Update paths if provided in context
68
+ if (context.servicePath) {
69
+ this.servicePath = context.servicePath;
70
+ }
71
+ if (context.templatesPath) {
72
+ this.templatesPath = context.templatesPath;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Extract and normalize context into consistent format
78
+ * @param {Object} context - Generation context
79
+ * @returns {Object} - Normalized context with coreInputs, confirmedValues, servicePath
80
+ */
81
+ extractContext(context) {
82
+ return {
83
+ coreInputs: context.coreInputs || {},
84
+ confirmedValues: context.confirmedValues || {},
85
+ servicePath: context.servicePath || this.servicePath || this.outputDir
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Get a value from the context
91
+ * @param {string} key - Context key (supports dot notation: 'config.name')
92
+ * @param {*} defaultValue - Default value if key not found
93
+ * @returns {*} - Context value or default
94
+ */
95
+ getContext(key, defaultValue = undefined) {
96
+ if (!key) return this.context;
97
+ const keys = key.split('.');
98
+ let value = this.context;
99
+ for (const k of keys) {
100
+ if (value && typeof value === 'object' && k in value) {
101
+ value = value[k];
102
+ } else {
103
+ return defaultValue;
104
+ }
105
+ }
106
+ return value;
107
+ }
108
+
109
+ /**
110
+ * Load a template file from the templates directory
111
+ * Subclasses can override this to implement custom template loading
112
+ * @param {string} templateName - Template filename or path relative to templatesPath
113
+ * @returns {Promise<string>} - Template content
114
+ */
115
+ async loadTemplate(templateName) {
116
+ if (!this.templatesPath) {
117
+ throw new Error(`templatesPath not set for ${this.name}`);
118
+ }
119
+ const templatePath = path.join(this.templatesPath, templateName);
120
+ try {
121
+ const content = await fs.readFile(templatePath, 'utf8');
122
+ return content;
123
+ } catch (error) {
124
+ const errorMessage = error instanceof Error ? error.message : String(error);
125
+ throw new Error(`Failed to load template '${templateName}' from '${templatePath}': ${errorMessage}`);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Render a template with variables
131
+ * Replaces {{variable}} placeholders with values from the variables object
132
+ * @param {string} template - Template string with {{placeholders}}
133
+ * @param {Object} variables - Variable values to replace
134
+ * @returns {string} - Rendered template
135
+ */
136
+ renderTemplate(template, variables = {}) {
137
+ if (typeof template !== 'string') {
138
+ throw new Error('Template must be a string');
139
+ }
140
+
141
+ // Merge context with provided variables (variables take precedence)
142
+ const mergedVars = {
143
+ ...this.context,
144
+ ...variables
145
+ };
146
+
147
+ // Replace {{variable}} placeholders
148
+ return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
149
+ const trimmedKey = key.trim();
150
+
151
+ // Check provided variables first (priority), then context
152
+ let value;
153
+ if (trimmedKey in variables) {
154
+ value = variables[trimmedKey];
155
+ } else {
156
+ // Support dot notation for context: {{config.name}}
157
+ value = this.getContext(trimmedKey);
158
+ }
159
+ if (value === undefined || value === null) {
160
+ this.logger.warn(`Template variable '${trimmedKey}' is undefined in ${this.name}`);
161
+ return match; // Keep placeholder if variable not found
162
+ }
163
+ return String(value);
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Write content to a file within the service directory
169
+ * Creates parent directories if they don't exist
170
+ * @param {string} relativePath - Path relative to servicePath
171
+ * @param {string} content - File content to write
172
+ * @param {Object} options - Write options
173
+ * @param {boolean} options.overwrite - Whether to overwrite existing files (default: true)
174
+ * @returns {Promise<void>}
175
+ */
176
+ async writeFile(relativePath, content, options = {}) {
177
+ if (!this.servicePath) {
178
+ throw new Error(`servicePath not set for ${this.name}`);
179
+ }
180
+ const fullPath = path.join(this.servicePath, relativePath);
181
+ const overwrite = options.overwrite !== false; // Default to true
182
+
183
+ // Check if file exists and overwrite is disabled
184
+ try {
185
+ await fs.access(fullPath);
186
+ if (!overwrite) {
187
+ this.logger.info(`Skipping existing file: ${relativePath}`);
188
+ return;
189
+ }
190
+ } catch {
191
+ // File doesn't exist, continue
192
+ }
193
+
194
+ // Ensure parent directory exists
195
+ const dir = path.dirname(fullPath);
196
+ await fs.mkdir(dir, {
197
+ recursive: true
198
+ });
199
+
200
+ // Write file
201
+ try {
202
+ await fs.writeFile(fullPath, content, 'utf8');
203
+ this.logger.info(`Generated: ${relativePath}`);
204
+ } catch (error) {
205
+ throw new Error(`Failed to write file '${relativePath}': ${error.message}`);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Log a message at info level
211
+ * @param {string} message - Message to log
212
+ */
213
+ log(message) {
214
+ this.logger.info(`[${this.name}] ${message}`);
215
+ }
216
+
217
+ /**
218
+ * Log a warning message
219
+ * @param {string} message - Warning message
220
+ */
221
+ warn(message) {
222
+ this.logger.warn(`[${this.name}] ${message}`);
223
+ }
224
+
225
+ /**
226
+ * Log an error message
227
+ * @param {string} message - Error message
228
+ */
229
+ error(message) {
230
+ this.logger.error(`[${this.name}] ${message}`);
231
+ }
232
+ }
233
+ export default BaseGenerator;
@@ -0,0 +1,254 @@
1
+ /**
2
+ * GeneratorRegistry - Registry for managing and executing generators
3
+ *
4
+ * Provides a centralized registry for all generators, organized by category.
5
+ * Controls execution order and manages generator lifecycle.
6
+ */
7
+ export class GeneratorRegistry {
8
+ /**
9
+ * Create a new generator registry
10
+ */
11
+ constructor() {
12
+ this.categories = new Map();
13
+ this.executionOrder = ['core',
14
+ // Core configuration files (package.json, wrangler.toml, etc.)
15
+ 'config',
16
+ // Environment and config files
17
+ 'code',
18
+ // Source code (schemas, handlers, middleware, utils)
19
+ 'scripts',
20
+ // Utility scripts (deploy, setup, health-check)
21
+ 'tests',
22
+ // Test files and test configuration
23
+ 'docs',
24
+ // Documentation files
25
+ 'ci',
26
+ // CI/CD workflows
27
+ 'service-types' // Service-type specific generation
28
+ ];
29
+ }
30
+
31
+ /**
32
+ * Register one or more generators in a category
33
+ * @param {string} category - Category name (e.g., 'core', 'config', 'code')
34
+ * @param {BaseGenerator|BaseGenerator[]} generators - Generator instance(s) to register
35
+ * @throws {Error} If category or generators are invalid
36
+ */
37
+ register(category, generators) {
38
+ if (!category || typeof category !== 'string') {
39
+ throw new Error('Category must be a non-empty string');
40
+ }
41
+ if (!generators) {
42
+ throw new Error('Generators must be provided');
43
+ }
44
+
45
+ // Ensure generators is an array
46
+ const generatorArray = Array.isArray(generators) ? generators : [generators];
47
+ if (generatorArray.length === 0) {
48
+ throw new Error('At least one generator must be provided');
49
+ }
50
+
51
+ // Validate all generators have a generate() method
52
+ for (const generator of generatorArray) {
53
+ if (!generator || typeof generator.generate !== 'function') {
54
+ throw new Error(`Invalid generator: must have a generate() method. Got: ${generator?.constructor?.name || typeof generator}`);
55
+ }
56
+ }
57
+
58
+ // Get existing generators for this category or create new array
59
+ const existing = this.categories.get(category) || [];
60
+
61
+ // Add new generators
62
+ this.categories.set(category, [...existing, ...generatorArray]);
63
+ }
64
+
65
+ /**
66
+ * Unregister a specific generator from a category
67
+ * @param {string} category - Category name
68
+ * @param {string} generatorName - Name of the generator to remove
69
+ * @returns {boolean} - True if generator was found and removed
70
+ */
71
+ unregister(category, generatorName) {
72
+ if (!this.categories.has(category)) {
73
+ return false;
74
+ }
75
+ const generators = this.categories.get(category);
76
+ const initialLength = generators.length;
77
+ const filtered = generators.filter(gen => gen.name !== generatorName);
78
+ if (filtered.length === 0) {
79
+ this.categories.delete(category);
80
+ } else {
81
+ this.categories.set(category, filtered);
82
+ }
83
+ return filtered.length < initialLength;
84
+ }
85
+
86
+ /**
87
+ * Get all generators for a specific category
88
+ * @param {string} category - Category name
89
+ * @returns {BaseGenerator[]} - Array of generators (empty if category not found)
90
+ */
91
+ getGenerators(category) {
92
+ return this.categories.get(category) || [];
93
+ }
94
+
95
+ /**
96
+ * Get all registered categories in execution order
97
+ * @returns {string[]} - Array of category names
98
+ */
99
+ getCategories() {
100
+ const registeredCategories = Array.from(this.categories.keys());
101
+
102
+ // Return categories in execution order, followed by any unordered categories
103
+ const ordered = this.executionOrder.filter(cat => registeredCategories.includes(cat));
104
+ const unordered = registeredCategories.filter(cat => !this.executionOrder.includes(cat));
105
+ return [...ordered, ...unordered];
106
+ }
107
+
108
+ /**
109
+ * Get total count of registered generators across all categories
110
+ * @returns {number} - Total generator count
111
+ */
112
+ getCount() {
113
+ let count = 0;
114
+ for (const generators of this.categories.values()) {
115
+ count += generators.length;
116
+ }
117
+ return count;
118
+ }
119
+
120
+ /**
121
+ * Get count of generators in a specific category
122
+ * @param {string} category - Category name
123
+ * @returns {number} - Generator count for category
124
+ */
125
+ getCategoryCount(category) {
126
+ return this.getGenerators(category).length;
127
+ }
128
+
129
+ /**
130
+ * Check if a category has any generators
131
+ * @param {string} category - Category name
132
+ * @returns {boolean} - True if category has generators
133
+ */
134
+ hasCategory(category) {
135
+ return this.categories.has(category) && this.categories.get(category).length > 0;
136
+ }
137
+
138
+ /**
139
+ * Clear all generators from a category
140
+ * @param {string} category - Category name
141
+ * @returns {boolean} - True if category existed and was cleared
142
+ */
143
+ clearCategory(category) {
144
+ return this.categories.delete(category);
145
+ }
146
+
147
+ /**
148
+ * Clear all generators from all categories
149
+ */
150
+ clearAll() {
151
+ this.categories.clear();
152
+ }
153
+
154
+ /**
155
+ * Execute all generators in order
156
+ * @param {Object} context - Generation context to pass to all generators
157
+ * @param {Object} options - Execution options
158
+ * @param {Function} options.logger - Logger for progress tracking
159
+ * @param {boolean} options.stopOnError - Whether to stop execution on first error (default: false)
160
+ * @returns {Promise<Object>} - Execution results { success, failed, skipped }
161
+ */
162
+ async execute(context, options = {}) {
163
+ const logger = options.logger || console;
164
+ const stopOnError = options.stopOnError !== false; // Default to true for safety
165
+
166
+ const results = {
167
+ success: [],
168
+ failed: [],
169
+ skipped: []
170
+ };
171
+ const categories = this.getCategories();
172
+ logger.info(`Starting generator execution: ${this.getCount()} generators in ${categories.length} categories`);
173
+ for (const category of categories) {
174
+ const generators = this.getGenerators(category);
175
+ logger.info(`Executing category: ${category} (${generators.length} generators)`);
176
+ for (const generator of generators) {
177
+ const name = generator.name || generator.constructor.name;
178
+ try {
179
+ // Check if generator should run
180
+ if (generator.shouldGenerate && !generator.shouldGenerate(context)) {
181
+ logger.info(`Skipping ${name}: shouldGenerate() returned false`);
182
+ results.skipped.push({
183
+ name,
184
+ category,
185
+ reason: 'shouldGenerate() returned false'
186
+ });
187
+ continue;
188
+ }
189
+
190
+ // Execute generator
191
+ logger.info(`Running ${name}...`);
192
+ await generator.generate(context);
193
+ results.success.push({
194
+ name,
195
+ category
196
+ });
197
+ logger.info(`✓ ${name} completed successfully`);
198
+ } catch (error) {
199
+ const errorInfo = {
200
+ name,
201
+ category,
202
+ error: error.message,
203
+ stack: error.stack
204
+ };
205
+ results.failed.push(errorInfo);
206
+ logger.error(`✗ ${name} failed: ${error.message}`);
207
+ if (stopOnError) {
208
+ logger.error('Stopping execution due to error (stopOnError=true)');
209
+ throw new Error(`Generator execution stopped: ${name} failed - ${error.message}`);
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ // Log summary
216
+ logger.info('\n=== Generator Execution Summary ===');
217
+ logger.info(`Total: ${this.getCount()} generators`);
218
+ logger.info(`✓ Success: ${results.success.length}`);
219
+ logger.info(`✗ Failed: ${results.failed.length}`);
220
+ logger.info(`⊘ Skipped: ${results.skipped.length}`);
221
+ if (results.failed.length > 0) {
222
+ logger.error('\nFailed generators:');
223
+ results.failed.forEach(({
224
+ name,
225
+ error
226
+ }) => {
227
+ logger.error(` - ${name}: ${error}`);
228
+ });
229
+ }
230
+ return results;
231
+ }
232
+
233
+ /**
234
+ * Get a summary of all registered generators
235
+ * @returns {Object} - Summary with categories and generator counts
236
+ */
237
+ getSummary() {
238
+ const categories = this.getCategories();
239
+ const summary = {
240
+ totalCategories: categories.length,
241
+ totalGenerators: this.getCount(),
242
+ categories: {}
243
+ };
244
+ for (const category of categories) {
245
+ const generators = this.getGenerators(category);
246
+ summary.categories[category] = {
247
+ count: generators.length,
248
+ generators: generators.map(g => g.name || g.constructor.name)
249
+ };
250
+ }
251
+ return summary;
252
+ }
253
+ }
254
+ export default GeneratorRegistry;
@@ -0,0 +1,87 @@
1
+ import { BaseGenerator } from '../BaseGenerator.js';
2
+ import { join } from 'path';
3
+ import { writeFileSync, mkdirSync } from 'fs';
4
+
5
+ /**
6
+ * CI Workflow Generator
7
+ * Generates GitHub Actions CI workflow for automated testing
8
+ */
9
+ export class CiWorkflowGenerator extends BaseGenerator {
10
+ /**
11
+ * Generate CI workflow
12
+ * @param {Object} context - Generation context
13
+ * @returns {Promise<string>} Path to generated CI workflow file
14
+ */
15
+ async generate(context) {
16
+ const {
17
+ coreInputs,
18
+ confirmedValues,
19
+ servicePath
20
+ } = this.extractContext(context);
21
+ if (!this.shouldGenerate(context)) {
22
+ return null;
23
+ }
24
+
25
+ // Ensure .github/workflows directory exists
26
+ const workflowsDir = join(servicePath, '.github', 'workflows');
27
+ mkdirSync(workflowsDir, {
28
+ recursive: true
29
+ });
30
+ const ciWorkflow = this._generateCiWorkflow(coreInputs, confirmedValues);
31
+ const filePath = join(workflowsDir, 'ci.yml');
32
+ writeFileSync(filePath, ciWorkflow, 'utf8');
33
+ return filePath;
34
+ }
35
+
36
+ /**
37
+ * Generate CI workflow content
38
+ * @private
39
+ */
40
+ _generateCiWorkflow(coreInputs, confirmedValues) {
41
+ return `name: CI
42
+
43
+ on:
44
+ push:
45
+ branches: [ main, master ]
46
+ pull_request:
47
+ branches: [ main, master ]
48
+
49
+ jobs:
50
+ test:
51
+ runs-on: ubuntu-latest
52
+
53
+ steps:
54
+ - uses: actions/checkout@v4
55
+
56
+ - name: Setup Node.js
57
+ uses: actions/setup-node@v4
58
+ with:
59
+ node-version: '18'
60
+ cache: 'npm'
61
+
62
+ - name: Install dependencies
63
+ run: npm ci
64
+
65
+ - name: Lint code
66
+ run: npm run lint
67
+
68
+ - name: Run tests
69
+ run: npm test
70
+
71
+ - name: Build
72
+ run: npm run build
73
+
74
+ - name: Upload coverage reports
75
+ uses: codecov/codecov-action@v3
76
+ with:
77
+ file: ./coverage/lcov.info
78
+ `;
79
+ }
80
+
81
+ /**
82
+ * Determine if generator should run
83
+ */
84
+ shouldGenerate(context) {
85
+ return true; // Always generate CI workflow
86
+ }
87
+ }