go-duck-cli 1.0.8 → 1.1.1

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 +481 -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,18 @@ 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
+ // Cleanup legacy files if they exist in the root
478
+ const legacyFiles = ['Dockerfile', 'docker-compose.yml'];
479
+ const legacyDirs = ['k8s', 'realm-config'];
480
+ for (const f of legacyFiles) {
481
+ const p = path.join(absoluteOutputDir, f);
482
+ if (await fs.pathExists(p)) await fs.remove(p);
483
+ }
484
+ for (const d of legacyDirs) {
485
+ const p = path.join(absoluteOutputDir, d);
486
+ if (await fs.pathExists(p)) await fs.remove(p);
487
+ }
488
+
352
489
  await generateConfigLoader(absoluteOutputDir);
353
490
  await generateLoggerCode(config, absoluteOutputDir);
354
491
  await generateMQTTCode(config, absoluteOutputDir);
@@ -357,48 +494,135 @@ program
357
494
  await generateTelemetryCode(config, absoluteOutputDir);
358
495
  await generateDeploymentArtifacts(config, absoluteOutputDir);
359
496
  await generateYAMLConfigs(config, absoluteOutputDir);
360
- const { entities, relationships, enums } = await generateEntities(path.join(path.resolve(process.cwd(), gdlDir), 'app.gdl'), absoluteOutputDir, config);
497
+ const { entities, relationships, enums, openEntities } = await generateEntities(path.resolve(process.cwd(), gdlDir), absoluteOutputDir, config);
361
498
  await generateKratosCode(entities, absoluteOutputDir, config.name, enums);
362
499
 
363
500
  await generateRepositoryCode(absoluteOutputDir);
501
+
502
+ {
503
+ const migratorTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/migrator.go.hbs');
504
+ if (await fs.pathExists(migratorTemplatePath)) {
505
+ const migratorTemplate = Handlebars.compile(await fs.readFile(migratorTemplatePath, 'utf8'));
506
+ await fs.writeFile(path.join(absoluteOutputDir, 'migrations/migrations.go'), migratorTemplate({ app_name: config.name }));
507
+ }
508
+ }
364
509
 
365
510
  await generateGraphQLCode(config, entities, relationships, absoluteOutputDir, enums);
366
- if (config.multitenancy?.enabled) await generateMultitenancy(config, absoluteOutputDir);
511
+ await generatePostgRESTCode(config, absoluteOutputDir);
512
+ if (config.multitenancy?.enabled) await generateMultitenancy(config, absoluteOutputDir, entities);
367
513
  await generateAuditCode(config, absoluteOutputDir);
368
514
  await generateMeteringCode(config, absoluteOutputDir);
369
515
  await generateSecurityMiddleware(config, absoluteOutputDir);
370
516
  await generateWebSocketCode(config, entities, absoluteOutputDir);
371
- await generatePostgRESTCode(config, absoluteOutputDir);
372
- console.log(chalk.green('āœ… PostgREST-like search layer created!'));
517
+ await generateOutbox(absoluteOutputDir, config);
518
+ await generateNATSCode(config, absoluteOutputDir);
519
+ await generateBrokerCode(config, absoluteOutputDir);
520
+ await generateStorageCode(config, absoluteOutputDir, path.dirname(path.resolve(process.cwd(), configPath)));
521
+ await generateRouterCode(absoluteOutputDir, config, entities, openEntities);
522
+ await generateElasticsearchLayer(config, entities, absoluteOutputDir);
523
+ await generateServerlessHandler(absoluteOutputDir, config);
524
+
525
+ console.log(chalk.green('\nšŸŽ‰ Go-Duck project generated successfully!'));
373
526
 
374
527
  // 8. Generate Swagger Docs
375
- await generateSwaggerDocs(config, entities, absoluteOutputDir);
376
- console.log(chalk.green('āœ… Swagger API documentation generated!'));
528
+ await generateSwaggerDocs(config, entities, absoluteOutputDir, openEntities);
529
+ await generatePostmanCollection(config, entities, absoluteOutputDir, openEntities);
530
+ console.log(chalk.green('āœ… Swagger API and Postman Collection generated!'));
377
531
 
378
532
  // 8.5 Generate Web Docs App
379
- await generateDocumentation(config, entities, absoluteOutputDir, enums);
533
+ await generateDocumentation(config, entities, absoluteOutputDir, enums, openEntities);
380
534
  console.log(chalk.green('āœ… Web Documentation App generated!'));
381
535
 
536
+ // 8.6 Generate AI/LLM Blueprint Documentation
537
+ await generateAIDocs(config, entities, absoluteOutputDir, enums, openEntities);
538
+ console.log(chalk.green('šŸ¤– System-level AI Blueprint Documentation generated!'));
539
+
382
540
  // 9. Generate main.go
383
541
  const mainTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/main.go.hbs');
384
542
  if (await fs.pathExists(mainTemplatePath)) {
385
543
  const mainTemplateSource = await fs.readFile(mainTemplatePath, 'utf8');
386
544
  const mainTemplate = Handlebars.compile(mainTemplateSource);
387
- await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities }));
545
+ await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities, openEntities }));
388
546
  console.log(chalk.green('āœ… main.go entry point created!'));
389
547
  }
548
+
549
+ // 10. Generate go.mod
550
+ const goModContent = `module ${config.name}
551
+
552
+ go 1.24
553
+
554
+ require (
555
+ github.com/gin-gonic/gin v1.10.0
556
+ github.com/go-kratos/kratos/v2 v2.8.2
557
+ github.com/google/uuid v1.6.0
558
+ github.com/spf13/viper v1.19.0
559
+ go.opentelemetry.io/otel v1.26.0
560
+ go.opentelemetry.io/otel/trace v1.26.0
561
+ go.opentelemetry.io/otel/sdk v1.26.0
562
+ go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.51.0
563
+ google.golang.org/grpc v1.64.0
564
+ gorm.io/datatypes v1.2.0
565
+ gorm.io/driver/postgres v1.5.7
566
+ gorm.io/gorm v1.25.10
567
+ gorm.io/plugin/opentelemetry v0.1.5
568
+ github.com/golang-jwt/jwt/v5 v5.2.1
569
+ github.com/eclipse/paho.mqtt.golang v1.4.3
570
+ github.com/sony/gobreaker v0.5.0
571
+ github.com/redis/go-redis/v9 v9.5.1
572
+ github.com/gorilla/websocket v1.5.1
573
+ github.com/99designs/gqlgen v0.17.44
574
+ github.com/vektah/gqlparser/v2 v2.5.11
575
+ github.com/aws/aws-lambda-go v1.47.0
576
+ github.com/awslabs/aws-lambda-go-api-proxy v0.16.1
577
+ github.com/GoogleCloudPlatform/functions-framework-go v1.8.1
578
+ go.mongodb.org/mongo-driver v1.17.1
579
+ github.com/gin-contrib/cors v1.7.1
580
+ github.com/go-resty/resty/v2 v2.16.0
581
+ github.com/olivere/elastic/v7 v7.0.32
582
+ github.com/nats-io/nats.go v1.38.0
583
+ )
584
+ `;
585
+ await fs.writeFile(path.join(absoluteOutputDir, 'go.mod'), goModContent);
586
+ console.log(chalk.green('āœ… go.mod file created!'));
587
+
588
+ // Post-generation: Run go mod tidy
589
+ try {
590
+ console.log(chalk.blue('Cleaning up Go modules (go mod tidy)...'));
591
+ execSync('go mod tidy', { cwd: absoluteOutputDir, stdio: 'inherit' });
592
+ console.log(chalk.green('āœ… Go modules tidied!'));
593
+ } catch (error) {
594
+ console.warn(chalk.yellow('āš ļø Could not run go mod tidy. Please run it manually.'));
595
+ }
596
+
390
597
  console.log(chalk.bold.magenta('\n✨ Project created successfully!'));
391
598
  });
392
599
 
393
600
  program
394
- .command('import-gdl <file>')
395
- .description('Import entities from a GDL file into an existing app')
601
+ .command('import-gdl <path>')
602
+ .description('Import entities from a GDL file or directory into an existing app')
396
603
  .option('-o, --output <path>', 'Path to the existing app root', '.')
397
- .action(async (file, options) => {
604
+ .action(async (gdlPath, options) => {
398
605
  const absoluteOutputDir = path.resolve(process.cwd(), options.output);
399
- console.log(chalk.blue(`šŸ“„ Importing GDL from ${file}...`));
606
+ console.log(chalk.blue(`šŸ“„ Importing GDL from ${gdlPath}...`));
607
+
608
+ let configPath = path.resolve(process.cwd(), './CONFIG/config.yaml');
609
+ if (!await fs.pathExists(configPath)) {
610
+ configPath = path.resolve(process.cwd(), '../CONFIG/config.yaml');
611
+ }
612
+ const config = await loadConfig(configPath);
613
+
614
+ // Cleanup legacy files if they exist in the root
615
+ const legacyFiles = ['Dockerfile', 'docker-compose.yml'];
616
+ const legacyDirs = ['k8s', 'realm-config'];
617
+ for (const f of legacyFiles) {
618
+ const p = path.join(absoluteOutputDir, f);
619
+ if (await fs.pathExists(p)) await fs.remove(p);
620
+ }
621
+ for (const d of legacyDirs) {
622
+ const p = path.join(absoluteOutputDir, d);
623
+ if (await fs.pathExists(p)) await fs.remove(p);
624
+ }
400
625
 
401
- const config = await loadConfig(path.resolve(process.cwd(), '../CONFIG/config.yaml'));
402
626
  await generateConfigLoader(absoluteOutputDir);
403
627
  await generateLoggerCode(config, absoluteOutputDir);
404
628
  await generateMQTTCode(config, absoluteOutputDir);
@@ -406,34 +630,52 @@ program
406
630
  await generateResilienceCode(config, absoluteOutputDir);
407
631
  await generateTelemetryCode(config, absoluteOutputDir);
408
632
  await generateDeploymentArtifacts(config, absoluteOutputDir);
409
- const { entities, relationships, enums } = await generateEntities(path.resolve(process.cwd(), file), absoluteOutputDir, config);
633
+ const { entities, relationships, enums, openEntities } = await generateEntities(gdlPath, absoluteOutputDir, config);
410
634
  await generateKratosCode(entities, absoluteOutputDir, config.name, enums);
411
635
 
412
636
  await generateRepositoryCode(absoluteOutputDir);
413
637
 
414
638
  await generateGraphQLCode(config, entities, relationships, absoluteOutputDir, enums);
415
- // Sync PostgREST search as well
416
639
  await generatePostgRESTCode(config, absoluteOutputDir);
417
640
 
418
- // Sync Security
641
+ // Sync Search & Security
642
+ await generateElasticsearchLayer(config, entities, absoluteOutputDir);
419
643
  await generateSecurityMiddleware(config, absoluteOutputDir);
420
644
 
421
645
  // Sync WebSocket
422
646
  await generateWebSocketCode(config, entities, absoluteOutputDir);
423
647
 
424
648
  // Sync Swagger Docs
425
- await generateSwaggerDocs(config, entities, absoluteOutputDir);
649
+ await generateSwaggerDocs(config, entities, absoluteOutputDir, openEntities);
650
+ await generatePostmanCollection(config, entities, absoluteOutputDir, openEntities);
426
651
 
427
652
  // Sync Web Docs App
428
- await generateDocumentation(config, entities, absoluteOutputDir, enums);
653
+ await generateDocumentation(config, entities, absoluteOutputDir, enums, openEntities);
654
+
655
+ // Sync AI Blueprints
656
+ await generateAIDocs(config, entities, absoluteOutputDir, enums, openEntities);
657
+ // Sync Serverless & Router
658
+ await generateRouterCode(absoluteOutputDir, config, entities, openEntities);
659
+ await generateServerlessHandler(absoluteOutputDir, config);
429
660
 
430
661
  // Regenerate main.go to include new routes if any (or just entities)
662
+ const timestamp = new Date().toISOString().replace(/[-:T]/g, '').split('.')[0];
431
663
  const mainTemplatePath = path.resolve(path.dirname(import.meta.url.replace('file://', '')), 'templates/go/main.go.hbs');
432
664
  if (await fs.pathExists(mainTemplatePath)) {
433
665
  const mainTemplate = Handlebars.compile(await fs.readFile(mainTemplatePath, 'utf8'));
434
- await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities }));
666
+ await fs.writeFile(path.join(absoluteOutputDir, 'main.go'), mainTemplate({ app_name: config.name, entities, openEntities, timestamp }));
435
667
  console.log(chalk.green('āœ… Updated main.go to register new entity routes.'));
436
668
  }
669
+
670
+ // Run go mod tidy after import as well
671
+ try {
672
+ console.log(chalk.blue('Cleaning up Go modules (go mod tidy)...'));
673
+ execSync('go mod tidy', { cwd: absoluteOutputDir, stdio: 'inherit' });
674
+ console.log(chalk.green('āœ… Go modules tidied!'));
675
+ } catch (error) {
676
+ console.warn(chalk.yellow('āš ļø Could not run go mod tidy. Please run it manually.'));
677
+ }
678
+
437
679
  console.log(chalk.bold.magenta('\n✨ GDL Import Completed with Incremental Migrations! ✨'));
438
680
  });
439
681
 
@@ -447,74 +689,159 @@ const generateYAMLConfigs = async (config, outputDir) => {
447
689
  const extendedConfig = {
448
690
  ...cleanConfig,
449
691
  server: {
450
- port: 8080,
451
- 'read-timeout': '30s',
452
- 'write-timeout': '30s',
692
+ port: cleanConfig.server?.port || 8080,
693
+ 'read-timeout': cleanConfig.server?.['read-timeout'] || '30s',
694
+ 'write-timeout': cleanConfig.server?.['write-timeout'] || '30s',
453
695
  grpc: {
454
- addr: ':9000',
455
- network: 'tcp',
456
- timeout: '1s'
696
+ addr: cleanConfig.server?.grpc?.addr || ':9000',
697
+ network: cleanConfig.server?.grpc?.network || 'tcp',
698
+ timeout: cleanConfig.server?.grpc?.timeout || '1s'
457
699
  },
458
700
  cors: {
459
- 'allow-origins': ['*'],
460
- 'allow-methods': ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
461
- 'allow-headers': ['Origin', 'Content-Type', 'Accept', 'Authorization']
701
+ 'allow-origins': cleanConfig.server?.cors?.['allow-origins'] || ['*'],
702
+ 'allow-methods': cleanConfig.server?.cors?.['allow-methods'] || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
703
+ 'allow-headers': cleanConfig.server?.cors?.['allow-headers'] || ['Origin', 'Content-Type', 'Accept', 'Authorization', 'X-Tenant-ID']
462
704
  }
463
705
  },
464
706
  datasource: {
465
707
  ...cleanConfig.datasource,
466
- 'max-open-conns': 25,
467
- 'max-idle-conns': 10,
468
- 'conn-max-lifetime': '5m'
708
+ 'ssl-mode': cleanConfig.datasource?.['ssl-mode'] || 'disable',
709
+ 'max-open-conns': cleanConfig.datasource?.['max-open-conns'] || 25,
710
+ 'max-idle-conns': cleanConfig.datasource?.['max-idle-conns'] || 10,
711
+ 'conn-max-lifetime': cleanConfig.datasource?.['conn-max-lifetime'] || '5m',
712
+ mongodb: {
713
+ enabled: cleanConfig.datasource?.mongodb?.enabled || false,
714
+ uri: cleanConfig.datasource?.mongodb?.uri || 'mongodb://localhost:27017',
715
+ database: cleanConfig.datasource?.mongodb?.database || cleanConfig.name || 'go_duck_mongo'
716
+ }
469
717
  },
470
718
  security: {
471
719
  ...cleanConfig.security,
720
+ 'keycloak-app-client-id': cleanConfig.security?.['keycloak-app-client-id'] || `${cleanConfig.name || 'go-duck'}-app`,
721
+ 'keycloak-app-client-secret': cleanConfig.security?.['keycloak-app-client-secret'] || 'app-secret-123',
722
+ 'keycloak-service-client-id': cleanConfig.security?.['keycloak-service-client-id'] || `${cleanConfig.name || 'go-duck'}-service`,
723
+ 'keycloak-service-secret': cleanConfig.security?.['keycloak-service-secret'] || 'service-secret-123',
724
+ 'keycloak-admin-client-id': cleanConfig.security?.['keycloak-admin-client-id'] || 'admin-cli',
725
+ 'keycloak-admin-secret': cleanConfig.security?.['keycloak-admin-secret'] || 'admin-secret-123',
726
+ 'super-admin-role': cleanConfig.security?.['super-admin-role'] || 'admin',
727
+ 'confidential-mode': cleanConfig.security?.['confidential-mode'] ?? true,
472
728
  'rate-limit': {
473
- rps: 100,
474
- burst: 200
729
+ rps: cleanConfig.security?.['rate-limit']?.rps || 100,
730
+ burst: cleanConfig.security?.['rate-limit']?.burst || 200
475
731
  }
476
732
  },
477
733
  logging: {
478
734
  datadog: {
479
- enabled: false,
480
- 'api-key': 'YOUR_DATADOG_API_KEY',
481
- site: 'datadoghq.com',
482
- service: cleanConfig.name || 'go-duck-service'
735
+ enabled: cleanConfig.logging?.datadog?.enabled || false,
736
+ 'api-key': cleanConfig.logging?.datadog?.['api-key'] || 'YOUR_DATADOG_API_KEY',
737
+ site: cleanConfig.logging?.datadog?.site || 'datadoghq.com',
738
+ service: cleanConfig.logging?.datadog?.service || cleanConfig.name || 'go-duck-service'
483
739
  }
484
740
  },
485
741
  messaging: {
486
742
  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'
743
+ enabled: cleanConfig.messaging?.mqtt?.enabled || false,
744
+ broker: cleanConfig.messaging?.mqtt?.broker || 'tcp://localhost:1883',
745
+ 'client-id': cleanConfig.messaging?.mqtt?.['client-id'] || (cleanConfig.name || 'go-duck') + '-dev',
746
+ username: cleanConfig.messaging?.mqtt?.username || 'dev_user',
747
+ password: cleanConfig.messaging?.mqtt?.password || 'dev_password',
748
+ 'topic-prefix': cleanConfig.messaging?.mqtt?.['topic-prefix'] || 'go-duck/events'
493
749
  }
494
750
  },
495
751
  cache: {
496
752
  redis: {
497
- enabled: false,
498
- host: 'localhost:6379',
499
- password: '',
500
- db: 0,
501
- ttl: '10m'
753
+ enabled: cleanConfig.cache?.redis?.enabled || false,
754
+ host: cleanConfig.cache?.redis?.host || 'localhost:6379',
755
+ password: cleanConfig.cache?.redis?.password || '',
756
+ db: cleanConfig.cache?.redis?.db || 0,
757
+ ttl: cleanConfig.cache?.redis?.ttl || '10m'
502
758
  }
503
759
  },
504
760
  resilience: {
505
761
  'circuit-breaker': {
506
- enabled: true,
507
- 'failure-threshold': 5,
508
- 'success-threshold': 2,
509
- timeout: '60s'
762
+ enabled: cleanConfig.resilience?.['circuit-breaker']?.enabled ?? true,
763
+ 'failure-threshold': cleanConfig.resilience?.['circuit-breaker']?.['failure-threshold'] || 5,
764
+ 'success-threshold': cleanConfig.resilience?.['circuit-breaker']?.['success-threshold'] || 2,
765
+ timeout: cleanConfig.resilience?.['circuit-breaker']?.timeout || '60s'
510
766
  }
511
767
  },
512
768
  telemetry: {
513
769
  otel: {
514
- enabled: false,
515
- endpoint: 'localhost:4317',
516
- 'sampler-ratio': 1.0
770
+ enabled: cleanConfig.telemetry?.otel?.enabled || false,
771
+ endpoint: cleanConfig.telemetry?.otel?.endpoint || 'localhost:4317',
772
+ 'sampler-ratio': cleanConfig.telemetry?.otel?.['sampler-ratio'] || 1.0
517
773
  }
774
+ },
775
+ storage: {
776
+ s3: {
777
+ enabled: cleanConfig.storage?.s3?.enabled || false,
778
+ bucket: cleanConfig.storage?.s3?.bucket || 'YOUR_S3_BUCKET',
779
+ region: cleanConfig.storage?.s3?.region || 'us-east-1',
780
+ 'access-key': cleanConfig.storage?.s3?.['access-key'] || 'YOUR_S3_ACCESS_KEY',
781
+ 'secret-key': cleanConfig.storage?.s3?.['secret-key'] || 'YOUR_S3_SECRET_KEY',
782
+ endpoint: cleanConfig.storage?.s3?.endpoint || ''
783
+ },
784
+ gcs: {
785
+ enabled: cleanConfig.storage?.gcs?.enabled || false,
786
+ bucket: cleanConfig.storage?.gcs?.bucket || 'YOUR_GCS_BUCKET',
787
+ 'credentials-file': cleanConfig.storage?.gcs?.['credentials-file'] || 'gcs-service-account.json'
788
+ },
789
+ minio: {
790
+ enabled: cleanConfig.storage?.minio?.enabled || false,
791
+ bucket: cleanConfig.storage?.minio?.bucket || 'YOUR_MINIO_BUCKET',
792
+ endpoint: cleanConfig.storage?.minio?.endpoint || 'http://localhost:9000',
793
+ 'access-key': cleanConfig.storage?.minio?.['access-key'] || 'YOUR_MINIO_ACCESS_KEY',
794
+ 'secret-key': cleanConfig.storage?.minio?.['secret-key'] || 'YOUR_MINIO_SECRET_KEY',
795
+ 'use-ssl': cleanConfig.storage?.minio?.['use-ssl'] || false
796
+ },
797
+ r2: {
798
+ enabled: cleanConfig.storage?.r2?.enabled || false,
799
+ bucket: cleanConfig.storage?.r2?.bucket || 'YOUR_R2_BUCKET',
800
+ 'account-id': cleanConfig.storage?.r2?.['account-id'] || 'YOUR_R2_ACCOUNT_ID',
801
+ 'access-key': cleanConfig.storage?.r2?.['access-key'] || 'YOUR_R2_ACCESS_KEY',
802
+ 'secret-key': cleanConfig.storage?.r2?.['secret-key'] || 'YOUR_R2_SECRET_KEY'
803
+ },
804
+ generic: {
805
+ enabled: cleanConfig.storage?.generic?.enabled || false,
806
+ provider: cleanConfig.storage?.generic?.provider || 'S3',
807
+ 'access-key': cleanConfig.storage?.generic?.['access-key'] || 'YOUR_ACCESS_KEY',
808
+ 'secret-key': cleanConfig.storage?.generic?.['secret-key'] || 'YOUR_SECRET_KEY'
809
+ },
810
+ sftp: {
811
+ enabled: cleanConfig.storage?.sftp?.enabled || false,
812
+ host: cleanConfig.storage?.sftp?.host || 'sftp.example.com',
813
+ port: cleanConfig.storage?.sftp?.port || 22,
814
+ username: cleanConfig.storage?.sftp?.username || 'sftp_user',
815
+ password: cleanConfig.storage?.sftp?.password || 'sftp_password',
816
+ 'key-file': cleanConfig.storage?.sftp?.['key-file'] || 'id_rsa',
817
+ 'remote-path': cleanConfig.storage?.sftp?.['remote-path'] || '/uploads'
818
+ },
819
+ github: {
820
+ enabled: cleanConfig.storage?.github?.enabled || false,
821
+ owner: cleanConfig.storage?.github?.owner || 'YOUR_ORG',
822
+ repo: cleanConfig.storage?.github?.repo || 'YOUR_REPO',
823
+ branch: cleanConfig.storage?.github?.branch || 'main',
824
+ path: cleanConfig.storage?.github?.path || '/',
825
+ token: cleanConfig.storage?.github?.token || 'YOUR_GITHUB_TOKEN',
826
+ username: cleanConfig.storage?.github?.username || '',
827
+ password: cleanConfig.storage?.github?.password || ''
828
+ }
829
+ },
830
+ serverless: {
831
+ enabled: cleanConfig.serverless?.enabled || false,
832
+ provider: cleanConfig.serverless?.provider || 'aws'
833
+ },
834
+ multitenancy: {
835
+ enabled: cleanConfig.multitenancy?.enabled || false,
836
+ 'hide-silo-names': cleanConfig.multitenancy?.['hide-silo-names'] || false
837
+ },
838
+ elasticsearch: {
839
+ enabled: cleanConfig.elasticsearch?.enabled || false,
840
+ addresses: cleanConfig.elasticsearch?.addresses || ['http://localhost:9200'],
841
+ username: cleanConfig.elasticsearch?.username || '',
842
+ password: cleanConfig.elasticsearch?.password || '',
843
+ auto_sync: cleanConfig.elasticsearch?.auto_sync ?? true,
844
+ index_prefix: cleanConfig.elasticsearch?.index_prefix || 'go_duck_'
518
845
  }
519
846
  };
520
847
 
@@ -534,54 +861,55 @@ const generateYAMLConfigs = async (config, outputDir) => {
534
861
  ...extendedConfig.server,
535
862
  cors: {
536
863
  ...extendedConfig.server.cors,
537
- 'allow-origins': ['https://your-domain.com']
864
+ 'allow-origins': cleanConfig.server?.cors?.['allow-origins'] || ['https://your-domain.com']
538
865
  }
539
866
  },
540
867
  logging: {
541
868
  ...extendedConfig.logging,
542
869
  datadog: {
543
870
  ...extendedConfig.logging.datadog,
544
- enabled: true
871
+ enabled: cleanConfig.logging?.datadog?.enabled ?? true
545
872
  }
546
873
  },
547
874
  messaging: {
548
875
  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'
876
+ ...extendedConfig.messaging.mqtt,
877
+ enabled: cleanConfig.messaging?.mqtt?.enabled ?? true,
878
+ broker: cleanConfig.messaging?.mqtt?.broker || 'tcp://mqtt.production.svc:1883',
879
+ 'client-id': cleanConfig.messaging?.mqtt?.['client-id'] || cleanConfig.name + '-prod'
555
880
  }
556
881
  },
557
882
  cache: {
558
883
  redis: {
559
- enabled: true,
560
- host: 'redis.production.svc:6379',
561
- password: 'prod_redis_password',
562
- db: 0,
563
- ttl: '1h'
884
+ ...extendedConfig.cache.redis,
885
+ enabled: cleanConfig.cache?.redis?.enabled ?? true,
886
+ host: cleanConfig.cache?.redis?.host || 'redis.production.svc:6379',
887
+ ttl: cleanConfig.cache?.redis?.ttl || '1h'
564
888
  }
565
889
  },
566
890
  resilience: {
567
891
  'circuit-breaker': {
568
- enabled: true,
569
- 'failure-threshold': 3,
570
- 'success-threshold': 5,
571
- timeout: '30s'
892
+ ...extendedConfig.resilience['circuit-breaker'],
893
+ enabled: cleanConfig.resilience?.['circuit-breaker']?.enabled ?? true,
894
+ 'failure-threshold': cleanConfig.resilience?.['circuit-breaker']?.['failure-threshold'] || 3,
895
+ 'success-threshold': cleanConfig.resilience?.['circuit-breaker']?.['success-threshold'] || 5
572
896
  }
573
897
  },
574
898
  telemetry: {
575
899
  otel: {
576
- enabled: true,
577
- endpoint: 'otel-collector.monitoring.svc:4317',
578
- 'sampler-ratio': 0.1
900
+ ...extendedConfig.telemetry.otel,
901
+ enabled: cleanConfig.telemetry?.otel?.enabled ?? true,
902
+ endpoint: cleanConfig.telemetry?.otel?.endpoint || 'otel-collector.monitoring.svc:4317',
903
+ 'sampler-ratio': cleanConfig.telemetry?.otel?.['sampler-ratio'] || 0.1
579
904
  }
580
905
  },
581
906
  datasource: {
582
907
  ...extendedConfig.datasource,
583
- 'max-open-conns': 100,
584
- 'max-idle-conns': 50
908
+ host: cleanConfig.datasource?.host || 'db.production.svc',
909
+ username: cleanConfig.datasource?.username || 'go_duck_user',
910
+ password: cleanConfig.datasource?.password || 'go_duck_pass',
911
+ 'max-open-conns': cleanConfig.datasource?.['max-open-conns'] || 100,
912
+ 'max-idle-conns': cleanConfig.datasource?.['max-idle-conns'] || 50
585
913
  }
586
914
  },
587
915
  environment: { active_profile: 'prod' }
@@ -589,4 +917,41 @@ const generateYAMLConfigs = async (config, outputDir) => {
589
917
  await fs.writeFile(path.join(outputDir, 'application-prod.yml'), yaml.dump(prodConfig));
590
918
  };
591
919
 
920
+ program
921
+ .command('serve')
922
+ .description('Birth of GO-DUCK: Start the interactive GUI and Documentation server on Port 2026')
923
+ .action(async () => {
924
+ const port = 2026;
925
+ const app = express();
926
+ const tempDir = path.join(process.cwd(), '.go_duck_birth');
927
+
928
+ console.log(chalk.blue('šŸ¦† Initializing Birth of GO-DUCK (Industrial GUI)...'));
929
+
930
+ // Load default config and dummy data for documentation preview
931
+ const config = { name: 'go-duck-preview' };
932
+ const entities = [];
933
+ const enums = [];
934
+ const openEntities = [];
935
+
936
+ await fs.ensureDir(tempDir);
937
+ await generateDocumentation(config, entities, tempDir, enums, openEntities);
938
+
939
+ const docsPath = path.join(tempDir, 'docs', 'web');
940
+ app.use(express.static(docsPath));
941
+
942
+ app.listen(port, async () => {
943
+ console.log(chalk.bold.green(`\nšŸš€ Birth of GO-DUCK Server is ALIVE!`));
944
+ console.log(chalk.cyan(`🌐 Documentation & Config Wizard: http://localhost:${port}`));
945
+ console.log(chalk.gray(`Press Ctrl+C to terminate the server.\n`));
946
+
947
+ await open(`http://localhost:${port}/wizard.html`);
948
+ });
949
+
950
+ // Cleanup temp files on exit
951
+ process.on('SIGINT', async () => {
952
+ await fs.remove(tempDir);
953
+ process.exit();
954
+ });
955
+ });
956
+
592
957
  program.parse(process.argv);