go-duck-cli 1.0.9 → 1.1.12

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/README.md +30 -15
  2. package/generators/ai_docs.js +130 -0
  3. package/generators/broker.js +63 -0
  4. package/generators/config.js +149 -7
  5. package/generators/devops.js +210 -43
  6. package/generators/docs.js +23 -4
  7. package/generators/elasticsearch.js +263 -0
  8. package/generators/kratos.js +229 -41
  9. package/generators/metering.js +280 -48
  10. package/generators/migrations.js +92 -198
  11. package/generators/mqtt.js +2 -39
  12. package/generators/multitenancy.js +274 -71
  13. package/generators/nats.js +39 -0
  14. package/generators/outbox.js +171 -0
  15. package/generators/postgrest.js +7 -3
  16. package/generators/postman.js +405 -0
  17. package/generators/repository.js +27 -0
  18. package/generators/router.js +27 -0
  19. package/generators/security.js +95 -14
  20. package/generators/serverless.js +147 -0
  21. package/generators/storage.js +589 -0
  22. package/generators/swagger.js +84 -60
  23. package/generators/telemetry.js +23 -32
  24. package/generators/websocket.js +55 -21
  25. package/index.js +493 -116
  26. package/package.json +6 -4
  27. package/parser/gdl.js +163 -24
  28. package/templates/docs/index.html.hbs +5 -5
  29. package/templates/docs/layout.hbs +221 -62
  30. package/templates/docs/pages/audit.hbs +83 -35
  31. package/templates/docs/pages/cli.hbs +18 -0
  32. package/templates/docs/pages/configuration.hbs +241 -0
  33. package/templates/docs/pages/datadog.hbs +46 -0
  34. package/templates/docs/pages/elasticsearch.hbs +121 -0
  35. package/templates/docs/pages/federation.hbs +241 -0
  36. package/templates/docs/pages/gdl-advanced.hbs +91 -0
  37. package/templates/docs/pages/gdl-annotations.hbs +137 -0
  38. package/templates/docs/pages/gdl-entities.hbs +134 -0
  39. package/templates/docs/pages/gdl-relationships.hbs +80 -0
  40. package/templates/docs/pages/gdl.hbs +60 -204
  41. package/templates/docs/pages/graphql.hbs +58 -44
  42. package/templates/docs/pages/grpc.hbs +53 -90
  43. package/templates/docs/pages/hybrid-store.hbs +127 -0
  44. package/templates/docs/pages/index.hbs +418 -149
  45. package/templates/docs/pages/keycloak.hbs +43 -0
  46. package/templates/docs/pages/legend.hbs +116 -0
  47. package/templates/docs/pages/mosquitto.hbs +39 -0
  48. package/templates/docs/pages/multitenancy.hbs +139 -71
  49. package/templates/docs/pages/otel.hbs +40 -0
  50. package/templates/docs/pages/realtime.hbs +38 -12
  51. package/templates/docs/pages/redis.hbs +40 -0
  52. package/templates/docs/pages/rest.hbs +120 -202
  53. package/templates/docs/pages/saga.hbs +94 -0
  54. package/templates/docs/pages/security.hbs +150 -44
  55. package/templates/docs/pages/serverless.hbs +157 -0
  56. package/templates/docs/pages/storage.hbs +127 -0
  57. package/templates/docs/pages/wizard.hbs +683 -0
  58. package/templates/docs/triple_identity_registry.png +0 -0
  59. package/templates/go/controller.go.hbs +287 -283
  60. package/templates/go/entity.go.hbs +17 -15
  61. package/templates/go/main.go.hbs +47 -180
  62. package/templates/go/migrator.go.hbs +65 -0
  63. package/templates/go/router.go.hbs +272 -0
  64. package/templates/graphql/resolver.go.hbs +53 -34
  65. package/templates/graphql/schema.graphql.hbs +17 -5
  66. package/templates/kratos/service.go.hbs +169 -34
  67. package/templates/proto/entity.proto.hbs +10 -14
  68. package/test_nested.gdl +21 -0
  69. package/templates/docs/intro.mp4 +0 -0
  70. package/test_parser.js +0 -9
package/index.js CHANGED
@@ -11,6 +11,9 @@ import path from 'path';
11
11
  import yaml from 'js-yaml';
12
12
  import Handlebars from 'handlebars';
13
13
  import chalk from 'chalk';
14
+ import { execSync } from 'child_process';
15
+ import express from 'express';
16
+ import open from 'open';
14
17
  import { parseGDL } from './parser/gdl.js';
15
18
  import { generateMultitenancy } from './generators/multitenancy.js';
16
19
  import { generateLiquibaseChangelogs } from './generators/migrations.js';
@@ -18,6 +21,7 @@ import { generateMeteringCode } from './generators/metering.js';
18
21
  import { generateGraphQLCode } from './generators/graphql.js';
19
22
  import { generatePostgRESTCode } from './generators/postgrest.js';
20
23
  import { generateSwaggerDocs } from './generators/swagger.js';
24
+ import { generatePostmanCollection } from './generators/postman.js';
21
25
  import { generateSecurityMiddleware } from './generators/security.js';
22
26
  import { generateWebSocketCode } from './generators/websocket.js';
23
27
  import { generateConfigLoader } from './generators/config.js';
@@ -30,6 +34,14 @@ import { generateDeploymentArtifacts } from './generators/devops.js';
30
34
  import { generateKratosCode } from './generators/kratos.js';
31
35
  import { generateRepositoryCode } from './generators/repository.js';
32
36
  import { generateDocumentation } from './generators/docs.js';
37
+ import { generateOutbox } from './generators/outbox.js';
38
+ import { generateNATSCode } from './generators/nats.js';
39
+ import { generateBrokerCode } from './generators/broker.js';
40
+ import { generateStorageCode } from './generators/storage.js';
41
+ import { generateElasticsearchLayer } from './generators/elasticsearch.js';
42
+ import { generateServerlessHandler } from './generators/serverless.js';
43
+ import { generateRouterCode } from './generators/router.js';
44
+ import { generateAIDocs } from './generators/ai_docs.js';
33
45
 
34
46
  export const generateAuditCode = async (config, outputDir) => {
35
47
  const middlewareDir = path.join(outputDir, 'middleware');
@@ -50,6 +62,7 @@ import (
50
62
  type AuditLog struct {
51
63
  ID uint \`gorm:"primaryKey" json:"id"\`
52
64
  EntityName string \`json:"entityName"\`
65
+ TenantDB string \`json:"tenantDb"\`
53
66
  EntityID string \`json:"entityId"\`
54
67
  Action string \`json:"action"\` // CREATE, UPDATE, DELETE
55
68
  PreviousValue string \`json:"previousValue" gorm:"type:text"\`
@@ -65,8 +78,7 @@ type AuditLog struct {
65
78
  package middleware
66
79
 
67
80
  import (
68
- "bytes"
69
- "io/ioutil"
81
+ "fmt"
70
82
  "net/http"
71
83
  "time"
72
84
 
@@ -77,7 +89,7 @@ import (
77
89
 
78
90
  func AuditMiddleware(db *gorm.DB) gin.HandlerFunc {
79
91
  return func(c *gin.Context) {
80
- if c.Request.Method == http.MethodGet {
92
+ if db == nil || c.Request.Method == http.MethodGet {
81
93
  c.Next()
82
94
  return
83
95
  }
@@ -91,23 +103,30 @@ func AuditMiddleware(db *gorm.DB) gin.HandlerFunc {
91
103
  if method == http.MethodPost { action = "CREATE" }
92
104
  if method == http.MethodDelete { action = "DELETE" }
93
105
 
94
- // Mock user and IP
95
- userEmail := c.GetHeader("User-Email")
96
- if userEmail == "" { userEmail = "anonymous" }
106
+ // Extract Identity from context
107
+ userEmail, _ := c.Get("UserEmail")
108
+ emailStr := "anonymous"
109
+ if email, ok := userEmail.(string); ok { emailStr = email }
110
+
111
+ keycloakId, _ := c.Get("KeycloakID")
112
+ kidStr := ""
113
+ if kid, ok := keycloakId.(string); ok { kidStr = kid }
97
114
 
98
- keycloakId := c.GetHeader("X-Keycloak-Id")
99
115
  clientIP := c.ClientIP()
100
116
 
117
+ tenant, _ := c.Get("tenantDB")
118
+ tenantStr := fmt.Sprintf("%v", tenant)
119
+
101
120
  // Call next handlers
102
121
  c.Next()
103
122
 
104
123
  // Logic to capture entity ID and snapshot values would go here...
105
- // For now, track the action
106
124
  auditEntry := models.AuditLog{
107
125
  EntityName: path,
126
+ TenantDB: tenantStr,
108
127
  Action: action,
109
- ModifiedBy: userEmail,
110
- KeycloakID: keycloakId,
128
+ ModifiedBy: emailStr,
129
+ KeycloakID: kidStr,
111
130
  ModifiedAt: time.Now(),
112
131
  ClientIP: clientIP,
113
132
  }
@@ -148,15 +167,26 @@ const program = new Command();
148
167
  // Handlebars Helpers
149
168
  Handlebars.registerHelper('capitalize', (str) => {
150
169
  if (typeof str !== 'string') return '';
151
- return str.charAt(0).toUpperCase() + str.slice(1);
170
+ // Handle snake_case by splitting and joining title-cased parts
171
+ return str.replace(/(?:^|_| )(\w)/g, (match, p1) => p1.toUpperCase());
172
+ });
173
+
174
+ Handlebars.registerHelper('toProtoFieldName', (str) => {
175
+ if (typeof str !== 'string') return '';
176
+ // Protoc-gen-go converts snake_case to TitleCase (e.g. site_id -> SiteId)
177
+ // But it ALSO removes underscores. So we split and capitalize.
178
+ return str.split('_').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
152
179
  });
153
180
 
154
181
  Handlebars.registerHelper('hasJson', (fields) => {
155
182
  if (!fields || !Array.isArray(fields)) return false;
156
- return fields.some(f => f.type === 'JSON' || f.type === 'JSONB');
183
+ return fields.some(f => (f.type || '').toLowerCase() === 'json' || (f.type || '').toLowerCase() === 'jsonb');
157
184
  });
158
185
 
159
- Handlebars.registerHelper('isJson', (type) => type === 'JSON' || type === 'JSONB');
186
+ Handlebars.registerHelper('isJson', (type) => {
187
+ const t = (type || '').toLowerCase();
188
+ return t === 'json' || t === 'jsonb';
189
+ });
160
190
 
161
191
  Handlebars.registerHelper('toLowerCase', (str) => {
162
192
  if (typeof str !== 'string') return '';
@@ -165,23 +195,30 @@ Handlebars.registerHelper('toLowerCase', (str) => {
165
195
 
166
196
  Handlebars.registerHelper('toGoType', (type, options) => {
167
197
  const enums = options.data.root.enums || [];
168
- const isEnum = enums.some(e => e.name === type);
169
- if (isEnum) return type;
198
+ const isEnum = enums.some(e => e.name.toLowerCase() === type.toLowerCase());
199
+ if (isEnum) {
200
+ const en = enums.find(e => e.name.toLowerCase() === type.toLowerCase());
201
+ return en.name;
202
+ }
170
203
 
204
+ const t = type.toLowerCase();
171
205
  const types = {
172
- 'String': 'string',
173
- 'Text': 'string',
174
- 'Integer': 'int',
175
- 'Float': 'float64',
176
- 'Boolean': 'bool',
177
- 'Long': 'int64',
178
- 'BigDecimal': 'float64',
179
- 'LocalDate': 'time.Time',
180
- 'Instant': 'time.Time',
181
- 'JSON': 'datatypes.JSON',
182
- 'JSONB': 'datatypes.JSON'
206
+ 'string': 'string',
207
+ 'text': 'string',
208
+ 'integer': 'int',
209
+ 'int': 'int',
210
+ 'float': 'float64',
211
+ 'boolean': 'bool',
212
+ 'bool': 'bool',
213
+ 'long': 'int64',
214
+ 'bigdecimal': 'float64',
215
+ 'localdate': 'time.Time',
216
+ 'instant': 'time.Time',
217
+ 'datetime': 'time.Time',
218
+ 'json': 'datatypes.JSON',
219
+ 'jsonb': 'datatypes.JSON'
183
220
  };
184
- return types[type] || 'interface{}';
221
+ return types[t] || 'interface{}';
185
222
  });
186
223
 
187
224
  Handlebars.registerHelper('gql_type', (type, options) => {
@@ -204,8 +241,53 @@ Handlebars.registerHelper('gql_type', (type, options) => {
204
241
  return types[type] || 'String';
205
242
  });
206
243
 
244
+ Handlebars.registerHelper('isAnyOperationOpen', (entityName, openEntities) => {
245
+ if (!openEntities || !Array.isArray(openEntities)) return false;
246
+ const wildcard = openEntities.find(e => e.name === '*');
247
+ if (wildcard && wildcard.actions && wildcard.actions.some(a => a !== 'none' && a !== '')) return true;
248
+ const entry = openEntities.find(e => e.name.toLowerCase() === entityName.toLowerCase());
249
+ return !!(entry && entry.actions && entry.actions.some(a => a !== 'none' && a !== ''));
250
+ });
251
+
252
+ Handlebars.registerHelper('isOpen', (entityName, openEntities, action) => {
253
+ if (!openEntities || !Array.isArray(openEntities)) return false;
254
+
255
+ // Check wildcard first
256
+ const wildcard = openEntities.find(e => e.name === '*');
257
+ if (wildcard) {
258
+ if (typeof action !== 'string') return true;
259
+ if (wildcard.actions.includes(action.toLowerCase())) return true;
260
+ }
261
+
262
+ const entry = openEntities.find(e => e.name.toLowerCase() === entityName.toLowerCase());
263
+ if (entry) {
264
+ if (typeof action !== 'string') return true;
265
+ if (entry.actions.includes(action.toLowerCase())) return true;
266
+ }
267
+
268
+ return false;
269
+ });
270
+
207
271
  Handlebars.registerHelper('eq', (a, b) => a === b);
208
272
 
273
+ Handlebars.registerHelper('or', function() {
274
+ const args = Array.prototype.slice.call(arguments, 0, -1);
275
+ return args.some(Boolean);
276
+ });
277
+
278
+ // Recursive field rendering for multi-level JSON
279
+ Handlebars.registerPartial('renderFields', `
280
+ {{#each fields}}
281
+ {{#if isNested}}
282
+ {{capitalize name}} struct {
283
+ {{> renderFields fields=children isDocument=../isDocument}}
284
+ } \`json:"{{name}}"{{#if ../isDocument}} bson:"{{name}}"{{/if}}\`
285
+ {{else}}
286
+ {{capitalize name}} {{toGoType type}} \`{{#if ../isDocument}}bson:"{{#if (eq name "id")}}_id{{else}}{{name}}{{/if}}{{#if (eq name "id")}},omitempty{{/if}}"{{else}}{{#if (eq name "id")}}gorm:"primaryKey"{{else}}{{#if unique}}gorm:"uniqueIndex"{{/if}}{{/if}}{{/if}} {{#if (isJson type)}}gorm:"type:{{toLowerCase type}};serializer:json"{{/if}} json:"{{name}}"{{#if required}} binding:"required"{{/if}}\`
287
+ {{/if}}
288
+ {{/each}}
289
+ `);
290
+
209
291
  program
210
292
  .name('go-duck-cli')
211
293
  .description('A powerful Go code generator for microservices')
@@ -216,7 +298,7 @@ const loadConfig = async (configPath) => {
216
298
  try {
217
299
  const fileContents = await fs.readFile(configPath, 'utf8');
218
300
  const config = yaml.load(fileContents);
219
- const appConfig = config.app || {};
301
+ const appConfig = config['go-duck'] || config.app || config || {};
220
302
  return appConfig;
221
303
  } catch (error) {
222
304
  console.error(chalk.red(`Error loading config from ${configPath}:`), error.message);
@@ -245,14 +327,46 @@ const getPreviousEntities = async (outputDir) => {
245
327
  return entities;
246
328
  };
247
329
 
248
- const generateEntities = async (gdlFilePath, outputDir, config) => {
249
- if (!await fs.pathExists(gdlFilePath)) {
250
- console.error(chalk.red(`āŒ GDL file not found: ${gdlFilePath}`));
251
- return null;
330
+ const generateEntities = async (gdlPath, outputDir, config) => {
331
+ let entities = [];
332
+ let relationships = [];
333
+ let enums = [];
334
+ let openEntities = [];
335
+
336
+ if (!await fs.pathExists(gdlPath)) {
337
+ console.log(chalk.yellow(`āš ļø GDL path not found: ${gdlPath} - Executing Zero-Entity Infrastructure Scaffold.`));
338
+ const delta = { newEntities: [], newFields: {}, newRelationships: [] };
339
+ await generateLiquibaseChangelogs(entities, relationships, outputDir, delta, enums);
340
+ return { entities, relationships, enums, openEntities };
341
+ }
342
+
343
+ const stats = await fs.stat(gdlPath);
344
+ const files = stats.isDirectory()
345
+ ? (await fs.readdir(gdlPath)).filter(f => f.endsWith('.gdl')).map(f => path.join(gdlPath, f))
346
+ : [gdlPath];
347
+
348
+ if (files.length === 0) {
349
+ console.log(chalk.yellow(`āš ļø No .gdl files found in: ${gdlPath} - Executing Zero-Entity Infrastructure Scaffold.`));
350
+ const delta = { newEntities: [], newFields: {}, newRelationships: [] };
351
+ await generateLiquibaseChangelogs(entities, relationships, outputDir, delta, enums);
352
+ return { entities, relationships, enums, openEntities };
353
+ }
354
+
355
+ console.log(chalk.blue(`šŸ“‚ Loading ${files.length} GDL file(s)...`));
356
+
357
+ for (const file of files) {
358
+ const result = await parseGDL(file);
359
+ entities = [...entities, ...result.entities];
360
+ relationships = [...relationships, ...result.relationships];
361
+ enums = [...enums, ...result.enums];
362
+ openEntities = [...openEntities, ...result.openEntities];
252
363
  }
253
364
 
254
- const { entities, relationships, enums } = await parseGDL(gdlFilePath);
255
- console.log(chalk.green(`āœ… Parsed ${entities.length} entities, ${relationships.length} relationships, and ${enums.length} enums`));
365
+ // Deduplicate entities and enums by name
366
+ entities = Array.from(new Map(entities.map(e => [e.name, e])).values());
367
+ enums = Array.from(new Map(enums.map(e => [e.name, e])).values());
368
+
369
+ console.log(chalk.green(`āœ… Parsed ${entities.length} entities, ${relationships.length} relationships, ${enums.length} enums, and detected ${openEntities.length} open rules`));
256
370
 
257
371
  const previousEntities = await getPreviousEntities(outputDir);
258
372
  const delta = {
@@ -308,9 +422,20 @@ const generateEntities = async (gdlFilePath, outputDir, config) => {
308
422
  }
309
423
 
310
424
  for (const entity of entities) {
311
- entity.relationships = relationships.filter(r => r.from.entity === entity.name || r.to.entity === entity.name);
425
+ entity.relationships = relationships.filter(r => r.from.entity === entity.name || r.to.entity === entity.name).map(r => {
426
+ const fromEnt = entities.find(e => e.name === r.from.entity);
427
+ const toEnt = entities.find(e => e.name === r.to.entity);
428
+ return {
429
+ ...r,
430
+ fromIsDocument: fromEnt ? fromEnt.isDocument : false,
431
+ toIsDocument: toEnt ? toEnt.isDocument : false
432
+ };
433
+ });
312
434
  entity.app_name = config.name;
313
- entity.enums = enums; // Pass enums context for helpers
435
+ entity.enums = enums;
436
+ entity.openEntities = openEntities; // Pass open entities to template
437
+ // Use already parsed flags from gdl.js
438
+ // entity.isSearchable, entity.isFederated, entity.isAudited, entity.isDocument are already set correctly.
314
439
 
315
440
  // Generate Model
316
441
  const entityContent = entityTemplate(entity);
@@ -320,7 +445,7 @@ const generateEntities = async (gdlFilePath, outputDir, config) => {
320
445
  const controllerContent = controllerTemplate(entity);
321
446
  await fs.writeFile(path.join(outputDir, 'controllers', `${entity.name.toLowerCase()}_controller.go`), controllerContent);
322
447
 
323
- console.log(chalk.gray(` - Updated Entity & Controller: ${entity.name}`));
448
+ console.log(chalk.gray(` - Updated Entity & Controller: ${entity.name} (Mongo: ${entity.isDocument || false})`));
324
449
 
325
450
  // Save Snapshot for next comparison
326
451
  await saveEntitySnapshot(outputDir, entity);
@@ -328,9 +453,9 @@ const generateEntities = async (gdlFilePath, outputDir, config) => {
328
453
 
329
454
  // Generate Incremental Changelogs!
330
455
  await generateLiquibaseChangelogs(entities, relationships, outputDir, delta, enums);
331
- console.log(chalk.green('āœ… Liquibase incremental migrations updated!'));
456
+ console.log(chalk.green('āœ… Goose SQL incremental migrations updated!'));
332
457
 
333
- return { entities, relationships, enums };
458
+ return { entities, relationships, enums, openEntities };
334
459
  };
335
460
 
336
461
  program
@@ -349,6 +474,27 @@ program
349
474
  const config = await loadConfig(path.resolve(process.cwd(), configPath));
350
475
  console.log(chalk.green(`āœ… Config loaded for app: ${config.name}`));
351
476
 
477
+ // Copy configuration file to the generated project .go-duck folder
478
+ const sourceConfigResolved = path.resolve(process.cwd(), configPath);
479
+ const targetConfigResolved = path.resolve(absoluteOutputDir, '.go-duck/config.yaml');
480
+ if (sourceConfigResolved !== targetConfigResolved) {
481
+ await fs.ensureDir(path.dirname(targetConfigResolved));
482
+ await fs.copy(sourceConfigResolved, targetConfigResolved);
483
+ console.log(chalk.green(`šŸ“ Config file copied to: ${path.relative(process.cwd(), targetConfigResolved)}`));
484
+ }
485
+
486
+ // Cleanup legacy files if they exist in the root
487
+ const legacyFiles = ['Dockerfile', 'docker-compose.yml'];
488
+ const legacyDirs = ['k8s', 'realm-config'];
489
+ for (const f of legacyFiles) {
490
+ const p = path.join(absoluteOutputDir, f);
491
+ if (await fs.pathExists(p)) await fs.remove(p);
492
+ }
493
+ for (const d of legacyDirs) {
494
+ const p = path.join(absoluteOutputDir, d);
495
+ if (await fs.pathExists(p)) await fs.remove(p);
496
+ }
497
+
352
498
  await generateConfigLoader(absoluteOutputDir);
353
499
  await generateLoggerCode(config, absoluteOutputDir);
354
500
  await generateMQTTCode(config, absoluteOutputDir);
@@ -357,48 +503,138 @@ program
357
503
  await generateTelemetryCode(config, absoluteOutputDir);
358
504
  await generateDeploymentArtifacts(config, absoluteOutputDir);
359
505
  await generateYAMLConfigs(config, absoluteOutputDir);
360
- const { entities, relationships, enums } = await generateEntities(path.join(path.resolve(process.cwd(), gdlDir), 'app.gdl'), absoluteOutputDir, config);
506
+ const { entities, relationships, enums, openEntities } = await generateEntities(path.resolve(process.cwd(), gdlDir), absoluteOutputDir, config);
361
507
  await generateKratosCode(entities, absoluteOutputDir, config.name, enums);
362
508
 
363
509
  await generateRepositoryCode(absoluteOutputDir);
510
+
511
+ {
512
+ const migratorTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/migrator.go.hbs');
513
+ if (await fs.pathExists(migratorTemplatePath)) {
514
+ const migratorTemplate = Handlebars.compile(await fs.readFile(migratorTemplatePath, 'utf8'));
515
+ await fs.writeFile(path.join(absoluteOutputDir, 'migrations/migrations.go'), migratorTemplate({ app_name: config.name }));
516
+ }
517
+ }
364
518
 
365
519
  await generateGraphQLCode(config, entities, relationships, absoluteOutputDir, enums);
366
- if (config.multitenancy?.enabled) await generateMultitenancy(config, absoluteOutputDir);
520
+ await generatePostgRESTCode(config, absoluteOutputDir);
521
+ if (config.multitenancy?.enabled) await generateMultitenancy(config, absoluteOutputDir, entities);
367
522
  await generateAuditCode(config, absoluteOutputDir);
368
523
  await generateMeteringCode(config, absoluteOutputDir);
369
524
  await generateSecurityMiddleware(config, absoluteOutputDir);
370
525
  await generateWebSocketCode(config, entities, absoluteOutputDir);
371
- await generatePostgRESTCode(config, absoluteOutputDir);
372
- console.log(chalk.green('āœ… PostgREST-like search layer created!'));
526
+ await generateOutbox(absoluteOutputDir, config);
527
+ await generateNATSCode(config, absoluteOutputDir);
528
+ await generateBrokerCode(config, absoluteOutputDir);
529
+ await generateStorageCode(config, absoluteOutputDir, path.dirname(path.resolve(process.cwd(), configPath)));
530
+ await generateRouterCode(absoluteOutputDir, config, entities, openEntities);
531
+ await generateElasticsearchLayer(config, entities, absoluteOutputDir);
532
+ await generateServerlessHandler(absoluteOutputDir, config);
533
+
534
+ console.log(chalk.green('\nšŸŽ‰ Go-Duck project generated successfully!'));
373
535
 
374
536
  // 8. Generate Swagger Docs
375
- await generateSwaggerDocs(config, entities, absoluteOutputDir);
376
- console.log(chalk.green('āœ… Swagger API documentation generated!'));
537
+ await generateSwaggerDocs(config, entities, absoluteOutputDir, openEntities);
538
+ await generatePostmanCollection(config, entities, absoluteOutputDir, openEntities);
539
+ console.log(chalk.green('āœ… Swagger API and Postman Collection generated!'));
377
540
 
378
541
  // 8.5 Generate Web Docs App
379
- await generateDocumentation(config, entities, absoluteOutputDir, enums);
542
+ await generateDocumentation(config, entities, absoluteOutputDir, enums, openEntities);
380
543
  console.log(chalk.green('āœ… Web Documentation App generated!'));
381
544
 
545
+ // 8.6 Generate AI/LLM Blueprint Documentation
546
+ await generateAIDocs(config, entities, absoluteOutputDir, enums, openEntities);
547
+ console.log(chalk.green('šŸ¤– System-level AI Blueprint Documentation generated!'));
548
+
382
549
  // 9. Generate main.go
383
550
  const mainTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/main.go.hbs');
384
551
  if (await fs.pathExists(mainTemplatePath)) {
385
552
  const mainTemplateSource = await fs.readFile(mainTemplatePath, 'utf8');
386
553
  const mainTemplate = Handlebars.compile(mainTemplateSource);
387
- await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities }));
554
+ await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities, openEntities }));
388
555
  console.log(chalk.green('āœ… main.go entry point created!'));
389
556
  }
557
+
558
+ // 10. Generate go.mod
559
+ const goModContent = `module ${config.name}
560
+
561
+ go 1.24
562
+
563
+ require (
564
+ github.com/gin-gonic/gin v1.10.0
565
+ github.com/go-kratos/kratos/v2 v2.8.2
566
+ github.com/google/uuid v1.6.0
567
+ github.com/spf13/viper v1.19.0
568
+ go.opentelemetry.io/otel v1.26.0
569
+ go.opentelemetry.io/otel/trace v1.26.0
570
+ go.opentelemetry.io/otel/sdk v1.26.0
571
+ go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.51.0
572
+ google.golang.org/grpc v1.64.0
573
+ gorm.io/datatypes v1.2.0
574
+ gorm.io/driver/postgres v1.5.7
575
+ gorm.io/gorm v1.25.10
576
+ gorm.io/plugin/opentelemetry v0.1.5
577
+ github.com/golang-jwt/jwt/v5 v5.2.1
578
+ github.com/eclipse/paho.mqtt.golang v1.4.3
579
+ github.com/sony/gobreaker v0.5.0
580
+ github.com/redis/go-redis/v9 v9.5.1
581
+ github.com/gorilla/websocket v1.5.1
582
+ github.com/99designs/gqlgen v0.17.44
583
+ github.com/vektah/gqlparser/v2 v2.5.11
584
+ github.com/aws/aws-lambda-go v1.47.0
585
+ github.com/awslabs/aws-lambda-go-api-proxy v0.16.1
586
+ github.com/GoogleCloudPlatform/functions-framework-go v1.8.1
587
+ go.mongodb.org/mongo-driver v1.17.1
588
+ github.com/gin-contrib/cors v1.7.1
589
+ github.com/go-resty/resty/v2 v2.16.0
590
+ github.com/olivere/elastic/v7 v7.0.32
591
+ github.com/nats-io/nats.go v1.38.0
592
+ )
593
+ `;
594
+ await fs.writeFile(path.join(absoluteOutputDir, 'go.mod'), goModContent);
595
+ console.log(chalk.green('āœ… go.mod file created!'));
596
+
597
+ // Post-generation: Run go mod tidy
598
+ try {
599
+ console.log(chalk.blue('Cleaning up Go modules (go mod tidy)...'));
600
+ execSync('go mod tidy', { cwd: absoluteOutputDir, stdio: 'inherit' });
601
+ console.log(chalk.green('āœ… Go modules tidied!'));
602
+ } catch (error) {
603
+ console.warn(chalk.yellow('āš ļø Could not run go mod tidy. Please run it manually.'));
604
+ }
605
+
390
606
  console.log(chalk.bold.magenta('\n✨ Project created successfully!'));
391
607
  });
392
608
 
393
609
  program
394
- .command('import-gdl <file>')
395
- .description('Import entities from a GDL file into an existing app')
610
+ .command('import-gdl <path>')
611
+ .description('Import entities from a GDL file or directory into an existing app')
396
612
  .option('-o, --output <path>', 'Path to the existing app root', '.')
397
- .action(async (file, options) => {
613
+ .action(async (gdlPath, options) => {
398
614
  const absoluteOutputDir = path.resolve(process.cwd(), options.output);
399
- console.log(chalk.blue(`šŸ“„ Importing GDL from ${file}...`));
615
+ console.log(chalk.blue(`šŸ“„ Importing GDL from ${gdlPath}...`));
616
+
617
+ let configPath = path.resolve(process.cwd(), './.go-duck/config.yaml');
618
+ if (!await fs.pathExists(configPath)) {
619
+ configPath = path.resolve(process.cwd(), './CONFIG/config.yaml');
620
+ }
621
+ if (!await fs.pathExists(configPath)) {
622
+ configPath = path.resolve(process.cwd(), '../CONFIG/config.yaml');
623
+ }
624
+ const config = await loadConfig(configPath);
625
+
626
+ // Cleanup legacy files if they exist in the root
627
+ const legacyFiles = ['Dockerfile', 'docker-compose.yml'];
628
+ const legacyDirs = ['k8s', 'realm-config'];
629
+ for (const f of legacyFiles) {
630
+ const p = path.join(absoluteOutputDir, f);
631
+ if (await fs.pathExists(p)) await fs.remove(p);
632
+ }
633
+ for (const d of legacyDirs) {
634
+ const p = path.join(absoluteOutputDir, d);
635
+ if (await fs.pathExists(p)) await fs.remove(p);
636
+ }
400
637
 
401
- const config = await loadConfig(path.resolve(process.cwd(), '../CONFIG/config.yaml'));
402
638
  await generateConfigLoader(absoluteOutputDir);
403
639
  await generateLoggerCode(config, absoluteOutputDir);
404
640
  await generateMQTTCode(config, absoluteOutputDir);
@@ -406,34 +642,52 @@ program
406
642
  await generateResilienceCode(config, absoluteOutputDir);
407
643
  await generateTelemetryCode(config, absoluteOutputDir);
408
644
  await generateDeploymentArtifacts(config, absoluteOutputDir);
409
- const { entities, relationships, enums } = await generateEntities(path.resolve(process.cwd(), file), absoluteOutputDir, config);
645
+ const { entities, relationships, enums, openEntities } = await generateEntities(gdlPath, absoluteOutputDir, config);
410
646
  await generateKratosCode(entities, absoluteOutputDir, config.name, enums);
411
647
 
412
648
  await generateRepositoryCode(absoluteOutputDir);
413
649
 
414
650
  await generateGraphQLCode(config, entities, relationships, absoluteOutputDir, enums);
415
- // Sync PostgREST search as well
416
651
  await generatePostgRESTCode(config, absoluteOutputDir);
417
652
 
418
- // Sync Security
653
+ // Sync Search & Security
654
+ await generateElasticsearchLayer(config, entities, absoluteOutputDir);
419
655
  await generateSecurityMiddleware(config, absoluteOutputDir);
420
656
 
421
657
  // Sync WebSocket
422
658
  await generateWebSocketCode(config, entities, absoluteOutputDir);
423
659
 
424
660
  // Sync Swagger Docs
425
- await generateSwaggerDocs(config, entities, absoluteOutputDir);
661
+ await generateSwaggerDocs(config, entities, absoluteOutputDir, openEntities);
662
+ await generatePostmanCollection(config, entities, absoluteOutputDir, openEntities);
426
663
 
427
664
  // Sync Web Docs App
428
- await generateDocumentation(config, entities, absoluteOutputDir, enums);
665
+ await generateDocumentation(config, entities, absoluteOutputDir, enums, openEntities);
666
+
667
+ // Sync AI Blueprints
668
+ await generateAIDocs(config, entities, absoluteOutputDir, enums, openEntities);
669
+ // Sync Serverless & Router
670
+ await generateRouterCode(absoluteOutputDir, config, entities, openEntities);
671
+ await generateServerlessHandler(absoluteOutputDir, config);
429
672
 
430
673
  // Regenerate main.go to include new routes if any (or just entities)
674
+ const timestamp = new Date().toISOString().replace(/[-:T]/g, '').split('.')[0];
431
675
  const mainTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/main.go.hbs');
432
676
  if (await fs.pathExists(mainTemplatePath)) {
433
677
  const mainTemplate = Handlebars.compile(await fs.readFile(mainTemplatePath, 'utf8'));
434
- await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities }));
678
+ await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities, openEntities, timestamp }));
435
679
  console.log(chalk.green('āœ… Updated main.go to register new entity routes.'));
436
680
  }
681
+
682
+ // Run go mod tidy after import as well
683
+ try {
684
+ console.log(chalk.blue('Cleaning up Go modules (go mod tidy)...'));
685
+ execSync('go mod tidy', { cwd: absoluteOutputDir, stdio: 'inherit' });
686
+ console.log(chalk.green('āœ… Go modules tidied!'));
687
+ } catch (error) {
688
+ console.warn(chalk.yellow('āš ļø Could not run go mod tidy. Please run it manually.'));
689
+ }
690
+
437
691
  console.log(chalk.bold.magenta('\n✨ GDL Import Completed with Incremental Migrations! ✨'));
438
692
  });
439
693
 
@@ -447,74 +701,159 @@ const generateYAMLConfigs = async (config, outputDir) => {
447
701
  const extendedConfig = {
448
702
  ...cleanConfig,
449
703
  server: {
450
- port: 8080,
451
- 'read-timeout': '30s',
452
- 'write-timeout': '30s',
704
+ port: cleanConfig.server?.port || 8080,
705
+ 'read-timeout': cleanConfig.server?.['read-timeout'] || '30s',
706
+ 'write-timeout': cleanConfig.server?.['write-timeout'] || '30s',
453
707
  grpc: {
454
- addr: ':9000',
455
- network: 'tcp',
456
- timeout: '1s'
708
+ addr: cleanConfig.server?.grpc?.addr || ':9000',
709
+ network: cleanConfig.server?.grpc?.network || 'tcp',
710
+ timeout: cleanConfig.server?.grpc?.timeout || '1s'
457
711
  },
458
712
  cors: {
459
- 'allow-origins': ['*'],
460
- 'allow-methods': ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
461
- 'allow-headers': ['Origin', 'Content-Type', 'Accept', 'Authorization']
713
+ 'allow-origins': cleanConfig.server?.cors?.['allow-origins'] || ['*'],
714
+ 'allow-methods': cleanConfig.server?.cors?.['allow-methods'] || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
715
+ 'allow-headers': cleanConfig.server?.cors?.['allow-headers'] || ['Origin', 'Content-Type', 'Accept', 'Authorization', 'X-Tenant-ID']
462
716
  }
463
717
  },
464
718
  datasource: {
465
719
  ...cleanConfig.datasource,
466
- 'max-open-conns': 25,
467
- 'max-idle-conns': 10,
468
- 'conn-max-lifetime': '5m'
720
+ 'ssl-mode': cleanConfig.datasource?.['ssl-mode'] || 'disable',
721
+ 'max-open-conns': cleanConfig.datasource?.['max-open-conns'] || 25,
722
+ 'max-idle-conns': cleanConfig.datasource?.['max-idle-conns'] || 10,
723
+ 'conn-max-lifetime': cleanConfig.datasource?.['conn-max-lifetime'] || '5m',
724
+ mongodb: {
725
+ enabled: cleanConfig.datasource?.mongodb?.enabled || false,
726
+ uri: cleanConfig.datasource?.mongodb?.uri || 'mongodb://localhost:27017',
727
+ database: cleanConfig.datasource?.mongodb?.database || cleanConfig.name || 'go_duck_mongo'
728
+ }
469
729
  },
470
730
  security: {
471
731
  ...cleanConfig.security,
732
+ 'keycloak-app-client-id': cleanConfig.security?.['keycloak-app-client-id'] || `${cleanConfig.name || 'go-duck'}-app`,
733
+ 'keycloak-app-client-secret': cleanConfig.security?.['keycloak-app-client-secret'] || 'app-secret-123',
734
+ 'keycloak-service-client-id': cleanConfig.security?.['keycloak-service-client-id'] || `${cleanConfig.name || 'go-duck'}-service`,
735
+ 'keycloak-service-secret': cleanConfig.security?.['keycloak-service-secret'] || 'service-secret-123',
736
+ 'keycloak-admin-client-id': cleanConfig.security?.['keycloak-admin-client-id'] || 'admin-cli',
737
+ 'keycloak-admin-secret': cleanConfig.security?.['keycloak-admin-secret'] || 'admin-secret-123',
738
+ 'super-admin-role': cleanConfig.security?.['super-admin-role'] || 'admin',
739
+ 'confidential-mode': cleanConfig.security?.['confidential-mode'] ?? true,
472
740
  'rate-limit': {
473
- rps: 100,
474
- burst: 200
741
+ rps: cleanConfig.security?.['rate-limit']?.rps || 100,
742
+ burst: cleanConfig.security?.['rate-limit']?.burst || 200
475
743
  }
476
744
  },
477
745
  logging: {
478
746
  datadog: {
479
- enabled: false,
480
- 'api-key': 'YOUR_DATADOG_API_KEY',
481
- site: 'datadoghq.com',
482
- service: cleanConfig.name || 'go-duck-service'
747
+ enabled: cleanConfig.logging?.datadog?.enabled || false,
748
+ 'api-key': cleanConfig.logging?.datadog?.['api-key'] || 'YOUR_DATADOG_API_KEY',
749
+ site: cleanConfig.logging?.datadog?.site || 'datadoghq.com',
750
+ service: cleanConfig.logging?.datadog?.service || cleanConfig.name || 'go-duck-service'
483
751
  }
484
752
  },
485
753
  messaging: {
486
754
  mqtt: {
487
- enabled: false,
488
- broker: 'tcp://localhost:1883',
489
- 'client-id': (cleanConfig.name || 'go-duck') + '-dev',
490
- username: 'dev_user',
491
- password: 'dev_password',
492
- 'topic-prefix': 'go-duck/events'
755
+ enabled: cleanConfig.messaging?.mqtt?.enabled || false,
756
+ broker: cleanConfig.messaging?.mqtt?.broker || 'tcp://localhost:1883',
757
+ 'client-id': cleanConfig.messaging?.mqtt?.['client-id'] || (cleanConfig.name || 'go-duck') + '-dev',
758
+ username: cleanConfig.messaging?.mqtt?.username || 'dev_user',
759
+ password: cleanConfig.messaging?.mqtt?.password || 'dev_password',
760
+ 'topic-prefix': cleanConfig.messaging?.mqtt?.['topic-prefix'] || 'go-duck/events'
493
761
  }
494
762
  },
495
763
  cache: {
496
764
  redis: {
497
- enabled: false,
498
- host: 'localhost:6379',
499
- password: '',
500
- db: 0,
501
- ttl: '10m'
765
+ enabled: cleanConfig.cache?.redis?.enabled || false,
766
+ host: cleanConfig.cache?.redis?.host || 'localhost:6379',
767
+ password: cleanConfig.cache?.redis?.password || '',
768
+ db: cleanConfig.cache?.redis?.db || 0,
769
+ ttl: cleanConfig.cache?.redis?.ttl || '10m'
502
770
  }
503
771
  },
504
772
  resilience: {
505
773
  'circuit-breaker': {
506
- enabled: true,
507
- 'failure-threshold': 5,
508
- 'success-threshold': 2,
509
- timeout: '60s'
774
+ enabled: cleanConfig.resilience?.['circuit-breaker']?.enabled ?? true,
775
+ 'failure-threshold': cleanConfig.resilience?.['circuit-breaker']?.['failure-threshold'] || 5,
776
+ 'success-threshold': cleanConfig.resilience?.['circuit-breaker']?.['success-threshold'] || 2,
777
+ timeout: cleanConfig.resilience?.['circuit-breaker']?.timeout || '60s'
510
778
  }
511
779
  },
512
780
  telemetry: {
513
781
  otel: {
514
- enabled: false,
515
- endpoint: 'localhost:4317',
516
- 'sampler-ratio': 1.0
782
+ enabled: cleanConfig.telemetry?.otel?.enabled || false,
783
+ endpoint: cleanConfig.telemetry?.otel?.endpoint || 'localhost:4317',
784
+ 'sampler-ratio': cleanConfig.telemetry?.otel?.['sampler-ratio'] || 1.0
785
+ }
786
+ },
787
+ storage: {
788
+ s3: {
789
+ enabled: cleanConfig.storage?.s3?.enabled || false,
790
+ bucket: cleanConfig.storage?.s3?.bucket || 'YOUR_S3_BUCKET',
791
+ region: cleanConfig.storage?.s3?.region || 'us-east-1',
792
+ 'access-key': cleanConfig.storage?.s3?.['access-key'] || 'YOUR_S3_ACCESS_KEY',
793
+ 'secret-key': cleanConfig.storage?.s3?.['secret-key'] || 'YOUR_S3_SECRET_KEY',
794
+ endpoint: cleanConfig.storage?.s3?.endpoint || ''
795
+ },
796
+ gcs: {
797
+ enabled: cleanConfig.storage?.gcs?.enabled || false,
798
+ bucket: cleanConfig.storage?.gcs?.bucket || 'YOUR_GCS_BUCKET',
799
+ 'credentials-file': cleanConfig.storage?.gcs?.['credentials-file'] || 'gcs-service-account.json'
800
+ },
801
+ minio: {
802
+ enabled: cleanConfig.storage?.minio?.enabled || false,
803
+ bucket: cleanConfig.storage?.minio?.bucket || 'YOUR_MINIO_BUCKET',
804
+ endpoint: cleanConfig.storage?.minio?.endpoint || 'http://localhost:9000',
805
+ 'access-key': cleanConfig.storage?.minio?.['access-key'] || 'YOUR_MINIO_ACCESS_KEY',
806
+ 'secret-key': cleanConfig.storage?.minio?.['secret-key'] || 'YOUR_MINIO_SECRET_KEY',
807
+ 'use-ssl': cleanConfig.storage?.minio?.['use-ssl'] || false
808
+ },
809
+ r2: {
810
+ enabled: cleanConfig.storage?.r2?.enabled || false,
811
+ bucket: cleanConfig.storage?.r2?.bucket || 'YOUR_R2_BUCKET',
812
+ 'account-id': cleanConfig.storage?.r2?.['account-id'] || 'YOUR_R2_ACCOUNT_ID',
813
+ 'access-key': cleanConfig.storage?.r2?.['access-key'] || 'YOUR_R2_ACCESS_KEY',
814
+ 'secret-key': cleanConfig.storage?.r2?.['secret-key'] || 'YOUR_R2_SECRET_KEY'
815
+ },
816
+ generic: {
817
+ enabled: cleanConfig.storage?.generic?.enabled || false,
818
+ provider: cleanConfig.storage?.generic?.provider || 'S3',
819
+ 'access-key': cleanConfig.storage?.generic?.['access-key'] || 'YOUR_ACCESS_KEY',
820
+ 'secret-key': cleanConfig.storage?.generic?.['secret-key'] || 'YOUR_SECRET_KEY'
821
+ },
822
+ sftp: {
823
+ enabled: cleanConfig.storage?.sftp?.enabled || false,
824
+ host: cleanConfig.storage?.sftp?.host || 'sftp.example.com',
825
+ port: cleanConfig.storage?.sftp?.port || 22,
826
+ username: cleanConfig.storage?.sftp?.username || 'sftp_user',
827
+ password: cleanConfig.storage?.sftp?.password || 'sftp_password',
828
+ 'key-file': cleanConfig.storage?.sftp?.['key-file'] || 'id_rsa',
829
+ 'remote-path': cleanConfig.storage?.sftp?.['remote-path'] || '/uploads'
830
+ },
831
+ github: {
832
+ enabled: cleanConfig.storage?.github?.enabled || false,
833
+ owner: cleanConfig.storage?.github?.owner || 'YOUR_ORG',
834
+ repo: cleanConfig.storage?.github?.repo || 'YOUR_REPO',
835
+ branch: cleanConfig.storage?.github?.branch || 'main',
836
+ path: cleanConfig.storage?.github?.path || '/',
837
+ token: cleanConfig.storage?.github?.token || 'YOUR_GITHUB_TOKEN',
838
+ username: cleanConfig.storage?.github?.username || '',
839
+ password: cleanConfig.storage?.github?.password || ''
517
840
  }
841
+ },
842
+ serverless: {
843
+ enabled: cleanConfig.serverless?.enabled || false,
844
+ provider: cleanConfig.serverless?.provider || 'aws'
845
+ },
846
+ multitenancy: {
847
+ enabled: cleanConfig.multitenancy?.enabled || false,
848
+ 'hide-silo-names': cleanConfig.multitenancy?.['hide-silo-names'] || false
849
+ },
850
+ elasticsearch: {
851
+ enabled: cleanConfig.elasticsearch?.enabled || false,
852
+ addresses: cleanConfig.elasticsearch?.addresses || ['http://localhost:9200'],
853
+ username: cleanConfig.elasticsearch?.username || '',
854
+ password: cleanConfig.elasticsearch?.password || '',
855
+ auto_sync: cleanConfig.elasticsearch?.auto_sync ?? true,
856
+ index_prefix: cleanConfig.elasticsearch?.index_prefix || 'go_duck_'
518
857
  }
519
858
  };
520
859
 
@@ -534,54 +873,55 @@ const generateYAMLConfigs = async (config, outputDir) => {
534
873
  ...extendedConfig.server,
535
874
  cors: {
536
875
  ...extendedConfig.server.cors,
537
- 'allow-origins': ['https://your-domain.com']
876
+ 'allow-origins': cleanConfig.server?.cors?.['allow-origins'] || ['https://your-domain.com']
538
877
  }
539
878
  },
540
879
  logging: {
541
880
  ...extendedConfig.logging,
542
881
  datadog: {
543
882
  ...extendedConfig.logging.datadog,
544
- enabled: true
883
+ enabled: cleanConfig.logging?.datadog?.enabled ?? true
545
884
  }
546
885
  },
547
886
  messaging: {
548
887
  mqtt: {
549
- enabled: true,
550
- broker: 'tcp://mqtt.production.svc:1883',
551
- 'client-id': cleanConfig.name + '-prod',
552
- username: 'prod_user',
553
- password: 'prod_password',
554
- 'topic-prefix': 'go-duck/events'
888
+ ...extendedConfig.messaging.mqtt,
889
+ enabled: cleanConfig.messaging?.mqtt?.enabled ?? true,
890
+ broker: cleanConfig.messaging?.mqtt?.broker || 'tcp://mqtt.production.svc:1883',
891
+ 'client-id': cleanConfig.messaging?.mqtt?.['client-id'] || cleanConfig.name + '-prod'
555
892
  }
556
893
  },
557
894
  cache: {
558
895
  redis: {
559
- enabled: true,
560
- host: 'redis.production.svc:6379',
561
- password: 'prod_redis_password',
562
- db: 0,
563
- ttl: '1h'
896
+ ...extendedConfig.cache.redis,
897
+ enabled: cleanConfig.cache?.redis?.enabled ?? true,
898
+ host: cleanConfig.cache?.redis?.host || 'redis.production.svc:6379',
899
+ ttl: cleanConfig.cache?.redis?.ttl || '1h'
564
900
  }
565
901
  },
566
902
  resilience: {
567
903
  'circuit-breaker': {
568
- enabled: true,
569
- 'failure-threshold': 3,
570
- 'success-threshold': 5,
571
- timeout: '30s'
904
+ ...extendedConfig.resilience['circuit-breaker'],
905
+ enabled: cleanConfig.resilience?.['circuit-breaker']?.enabled ?? true,
906
+ 'failure-threshold': cleanConfig.resilience?.['circuit-breaker']?.['failure-threshold'] || 3,
907
+ 'success-threshold': cleanConfig.resilience?.['circuit-breaker']?.['success-threshold'] || 5
572
908
  }
573
909
  },
574
910
  telemetry: {
575
911
  otel: {
576
- enabled: true,
577
- endpoint: 'otel-collector.monitoring.svc:4317',
578
- 'sampler-ratio': 0.1
912
+ ...extendedConfig.telemetry.otel,
913
+ enabled: cleanConfig.telemetry?.otel?.enabled ?? true,
914
+ endpoint: cleanConfig.telemetry?.otel?.endpoint || 'otel-collector.monitoring.svc:4317',
915
+ 'sampler-ratio': cleanConfig.telemetry?.otel?.['sampler-ratio'] || 0.1
579
916
  }
580
917
  },
581
918
  datasource: {
582
919
  ...extendedConfig.datasource,
583
- 'max-open-conns': 100,
584
- 'max-idle-conns': 50
920
+ host: cleanConfig.datasource?.host || 'db.production.svc',
921
+ username: cleanConfig.datasource?.username || 'go_duck_user',
922
+ password: cleanConfig.datasource?.password || 'go_duck_pass',
923
+ 'max-open-conns': cleanConfig.datasource?.['max-open-conns'] || 100,
924
+ 'max-idle-conns': cleanConfig.datasource?.['max-idle-conns'] || 50
585
925
  }
586
926
  },
587
927
  environment: { active_profile: 'prod' }
@@ -589,4 +929,41 @@ const generateYAMLConfigs = async (config, outputDir) => {
589
929
  await fs.writeFile(path.join(outputDir, 'application-prod.yml'), yaml.dump(prodConfig));
590
930
  };
591
931
 
932
+ program
933
+ .command('serve')
934
+ .description('Birth of GO-DUCK: Start the interactive GUI and Documentation server on Port 2026')
935
+ .action(async () => {
936
+ const port = 2026;
937
+ const app = express();
938
+ const tempDir = path.join(process.cwd(), '.go_duck_birth');
939
+
940
+ console.log(chalk.blue('šŸ¦† Initializing Birth of GO-DUCK (Industrial GUI)...'));
941
+
942
+ // Load default config and dummy data for documentation preview
943
+ const config = { name: 'go-duck-preview' };
944
+ const entities = [];
945
+ const enums = [];
946
+ const openEntities = [];
947
+
948
+ await fs.ensureDir(tempDir);
949
+ await generateDocumentation(config, entities, tempDir, enums, openEntities);
950
+
951
+ const docsPath = path.join(tempDir, 'docs', 'web');
952
+ app.use(express.static(docsPath));
953
+
954
+ app.listen(port, async () => {
955
+ console.log(chalk.bold.green(`\nšŸš€ Birth of GO-DUCK Server is ALIVE!`));
956
+ console.log(chalk.cyan(`🌐 Documentation & Config Wizard: http://localhost:${port}`));
957
+ console.log(chalk.gray(`Press Ctrl+C to terminate the server.\n`));
958
+
959
+ await open(`http://localhost:${port}/wizard.html`);
960
+ });
961
+
962
+ // Cleanup temp files on exit
963
+ process.on('SIGINT', async () => {
964
+ await fs.remove(tempDir);
965
+ process.exit();
966
+ });
967
+ });
968
+
592
969
  program.parse(process.argv);