eva4j 1.0.13 → 1.0.15

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 (106) hide show
  1. package/AGENTS.md +314 -10
  2. package/COMMAND_EVALUATION.md +15 -16
  3. package/DOMAIN_YAML_GUIDE.md +576 -10
  4. package/FUTURE_FEATURES.md +1627 -1168
  5. package/README.md +318 -13
  6. package/bin/eva4j.js +34 -0
  7. package/config/defaults.json +1 -0
  8. package/design-system.md +797 -0
  9. package/docs/commands/EVALUATE_SYSTEM.md +994 -0
  10. package/docs/commands/GENERATE_ENTITIES.md +795 -6
  11. package/docs/commands/INDEX.md +10 -1
  12. package/examples/domain-endpoints-relations.yaml +353 -0
  13. package/examples/domain-endpoints-versioned.yaml +144 -0
  14. package/examples/domain-endpoints.yaml +135 -0
  15. package/examples/domain-events.yaml +166 -20
  16. package/examples/domain-listeners.yaml +212 -0
  17. package/examples/domain-one-to-many.yaml +1 -0
  18. package/examples/domain-one-to-one.yaml +1 -0
  19. package/examples/domain-ports.yaml +414 -0
  20. package/examples/domain-soft-delete.yaml +47 -44
  21. package/examples/system/notification.yaml +147 -0
  22. package/examples/system/product.yaml +185 -0
  23. package/examples/system/system.yaml +112 -0
  24. package/examples/system-report.html +971 -0
  25. package/examples/system.yaml +332 -0
  26. package/package.json +2 -1
  27. package/src/commands/build.js +714 -0
  28. package/src/commands/create.js +7 -3
  29. package/src/commands/detach.js +1 -0
  30. package/src/commands/evaluate-system.js +610 -0
  31. package/src/commands/generate-entities.js +1331 -49
  32. package/src/commands/generate-http-exchange.js +2 -0
  33. package/src/commands/generate-kafka-event.js +98 -11
  34. package/src/generators/base-generator.js +8 -1
  35. package/src/generators/postman-generator.js +188 -0
  36. package/src/generators/shared-generator.js +10 -0
  37. package/src/utils/config-manager.js +54 -0
  38. package/src/utils/context-builder.js +1 -0
  39. package/src/utils/domain-diagram.js +192 -0
  40. package/src/utils/domain-validator.js +970 -0
  41. package/src/utils/fake-data.js +376 -0
  42. package/src/utils/naming.js +3 -2
  43. package/src/utils/system-validator.js +434 -0
  44. package/src/utils/yaml-to-entity.js +302 -8
  45. package/templates/aggregate/AggregateMapper.java.ejs +3 -2
  46. package/templates/aggregate/AggregateRepository.java.ejs +8 -2
  47. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +13 -3
  48. package/templates/aggregate/AggregateRoot.java.ejs +60 -2
  49. package/templates/aggregate/DomainEventHandler.java.ejs +27 -20
  50. package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
  51. package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
  52. package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
  53. package/templates/aggregate/JpaRepository.java.ejs +5 -0
  54. package/templates/base/gradle/build.gradle.ejs +3 -2
  55. package/templates/base/root/AGENTS.md.ejs +306 -45
  56. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1663 -0
  57. package/templates/base/root/skill-build-system-yaml.ejs +1446 -0
  58. package/templates/base/root/system.yaml.ejs +97 -0
  59. package/templates/crud/ApplicationMapper.java.ejs +4 -0
  60. package/templates/crud/Controller.java.ejs +4 -4
  61. package/templates/crud/CreateCommand.java.ejs +4 -0
  62. package/templates/crud/CreateItemDto.java.ejs +4 -0
  63. package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
  64. package/templates/crud/DeleteCommandHandler.java.ejs +10 -2
  65. package/templates/crud/EndpointsController.java.ejs +178 -0
  66. package/templates/crud/FindByQuery.java.ejs +17 -0
  67. package/templates/crud/FindByQueryHandler.java.ejs +57 -0
  68. package/templates/crud/ListQuery.java.ejs +1 -1
  69. package/templates/crud/ListQueryHandler.java.ejs +8 -8
  70. package/templates/crud/ScaffoldCommand.java.ejs +12 -0
  71. package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
  72. package/templates/crud/ScaffoldQuery.java.ejs +13 -0
  73. package/templates/crud/ScaffoldQueryHandler.java.ejs +41 -0
  74. package/templates/crud/SubEntityAddCommand.java.ejs +21 -0
  75. package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
  76. package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
  77. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
  78. package/templates/crud/TransitionCommand.java.ejs +9 -0
  79. package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
  80. package/templates/crud/UpdateCommand.java.ejs +4 -0
  81. package/templates/evaluate/report.html.ejs +1363 -0
  82. package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
  83. package/templates/kafka-event/Event.java.ejs +16 -0
  84. package/templates/kafka-listener/KafkaController.java.ejs +1 -1
  85. package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
  86. package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
  87. package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
  88. package/templates/kafka-listener/ListenerCommandHandler.java.ejs +23 -0
  89. package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
  90. package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
  91. package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
  92. package/templates/mock/MockEvent.java.ejs +10 -0
  93. package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
  94. package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
  95. package/templates/mock/SpringEventListener.java.ejs +61 -0
  96. package/templates/ports/PortDomainModel.java.ejs +35 -0
  97. package/templates/ports/PortFeignAdapter.java.ejs +67 -0
  98. package/templates/ports/PortFeignClient.java.ejs +45 -0
  99. package/templates/ports/PortFeignConfig.java.ejs +24 -0
  100. package/templates/ports/PortInterface.java.ejs +45 -0
  101. package/templates/ports/PortNestedType.java.ejs +28 -0
  102. package/templates/ports/PortRequestDto.java.ejs +30 -0
  103. package/templates/ports/PortResponseDto.java.ejs +28 -0
  104. package/templates/postman/Collection.json.ejs +1 -1
  105. package/templates/postman/UnifiedCollection.json.ejs +185 -0
  106. package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
@@ -0,0 +1,714 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+ const yaml = require('js-yaml');
7
+
8
+ const ConfigManager = require('../utils/config-manager');
9
+ const { isEva4jProject } = require('../utils/validator');
10
+ const { toCamelCase, toPackagePath } = require('../utils/naming');
11
+ const SharedGenerator = require('../generators/shared-generator');
12
+ const { renderAndWrite } = require('../utils/template-engine');
13
+ const addModuleCommand = require('./add-module');
14
+ const addKafkaClientCommand = require('./add-kafka-client');
15
+ const generateEntitiesCommand = require('./generate-entities');
16
+ const { generateUnifiedPostmanCollection } = require('../generators/postman-generator');
17
+
18
+ // ── H2 mock config ─────────────────────────────────────────────────────────────
19
+ const H2_DB_YAML = (packageName) => `spring:
20
+ datasource:
21
+ url: jdbc:h2:file:./data/mockdb;AUTO_SERVER=TRUE
22
+ username: sa
23
+ password: ''
24
+ driver-class-name: org.h2.Driver
25
+ h2:
26
+ console:
27
+ enabled: true
28
+ jpa:
29
+ hibernate:
30
+ ddl-auto: update
31
+ show-sql: true
32
+ properties:
33
+ hibernate:
34
+ format_sql: true
35
+ dialect: org.hibernate.dialect.H2Dialect
36
+
37
+ logging:
38
+ level:
39
+ root: INFO
40
+ ${packageName}: DEBUG
41
+ `;
42
+
43
+ const H2_GRADLE_LINE = ` runtimeOnly 'com.h2database:h2'`;
44
+ const ENVS = ['local', 'develop', 'test', 'production'];
45
+
46
+ /**
47
+ * Rebuild db context from projectConfig fields (mirrors detach.js logic).
48
+ */
49
+ function buildDbContext(projectConfig) {
50
+ const databaseType = projectConfig.databaseType || 'postgresql';
51
+ const databaseName = (projectConfig.artifactId || projectConfig.projectName || 'app').replace(/-/g, '_');
52
+
53
+ const dbMap = {
54
+ h2: {
55
+ driver: 'com.h2.database:h2',
56
+ driverClass: 'org.h2.Driver',
57
+ url: `jdbc:h2:mem:${databaseName}`,
58
+ username: 'sa',
59
+ password: '',
60
+ hibernateDialect: 'org.hibernate.dialect.H2Dialect',
61
+ },
62
+ postgresql: {
63
+ driver: 'org.postgresql:postgresql',
64
+ driverClass: 'org.postgresql.Driver',
65
+ url: `jdbc:postgresql://localhost:5432/${databaseName}`,
66
+ username: 'postgres',
67
+ password: 'postgres',
68
+ hibernateDialect: 'org.hibernate.dialect.PostgreSQLDialect',
69
+ },
70
+ mysql: {
71
+ driver: 'com.mysql:mysql-connector-j',
72
+ driverClass: 'com.mysql.cj.jdbc.Driver',
73
+ url: `jdbc:mysql://localhost:3306/${databaseName}`,
74
+ username: 'root',
75
+ password: 'root',
76
+ hibernateDialect: 'org.hibernate.dialect.MySQLDialect',
77
+ },
78
+ };
79
+
80
+ const db = dbMap[databaseType] || dbMap.postgresql;
81
+ return {
82
+ dependencies: projectConfig.dependencies || ['data-jpa'],
83
+ packageName: projectConfig.packageName,
84
+ databaseType,
85
+ databaseName,
86
+ databaseDriverClass: db.driverClass,
87
+ databaseUrl: db.url,
88
+ databaseUsername: db.username,
89
+ databasePassword: db.password,
90
+ hibernateDialect: db.hibernateDialect,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Regenerate db.yaml files from EJS templates using project's original DB config.
96
+ * Guarantees correctness even if backup contained stale/wrong content.
97
+ */
98
+ async function regenerateDbYaml(projectDir, projectConfig) {
99
+ const dbContext = buildDbContext(projectConfig);
100
+ const templatesDir = path.join(__dirname, '../../templates/base');
101
+ const resourcesPath = path.join(projectDir, 'src', 'main', 'resources');
102
+
103
+ for (const env of ENVS) {
104
+ const templatePath = path.join(templatesDir, 'resources', 'parameters', env, 'db.yaml.ejs');
105
+ const destPath = path.join(resourcesPath, 'parameters', env, 'db.yaml');
106
+ if (await fs.pathExists(templatePath)) {
107
+ await renderAndWrite(templatePath, destPath, dbContext);
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Rebuild the runtimeOnly DB driver line in build.gradle from project's original DB type.
114
+ */
115
+ async function regenerateBuildGradleDbDriver(projectDir, projectConfig) {
116
+ const databaseType = projectConfig.databaseType || 'postgresql';
117
+ const driverMap = {
118
+ h2: ` runtimeOnly 'com.h2.database:h2'`,
119
+ postgresql: ` runtimeOnly 'org.postgresql:postgresql'`,
120
+ mysql: ` runtimeOnly 'com.mysql:mysql-connector-j'`,
121
+ };
122
+ const correctLine = driverMap[databaseType] || driverMap.postgresql;
123
+
124
+ const buildGradlePath = path.join(projectDir, 'build.gradle');
125
+ if (await fs.pathExists(buildGradlePath)) {
126
+ const current = await fs.readFile(buildGradlePath, 'utf-8');
127
+ const fixed = current.replace(
128
+ /^[ \t]*runtimeOnly\s+['"][^'"]+['"]\s*(?:\/\/.*)?$/m,
129
+ correctLine
130
+ );
131
+ await fs.writeFile(buildGradlePath, fixed, 'utf-8');
132
+ }
133
+ }
134
+
135
+ const H2_SECURITY_CONFIG = (packageName) => {
136
+ const S = '$'; // prevent JS template interpolation of Spring EL ${...}
137
+ return `package ${packageName}.shared.infrastructure.configurations.securityConfig;
138
+
139
+ import org.springframework.beans.factory.annotation.Value;
140
+ import org.springframework.context.annotation.Bean;
141
+ import org.springframework.context.annotation.Configuration;
142
+ import org.springframework.security.config.Customizer;
143
+ import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
144
+ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
145
+ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
146
+ import org.springframework.security.config.http.SessionCreationPolicy;
147
+ import org.springframework.security.web.SecurityFilterChain;
148
+ import org.springframework.web.cors.CorsConfiguration;
149
+ import org.springframework.web.cors.CorsConfigurationSource;
150
+ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
151
+
152
+ import java.util.List;
153
+
154
+ // ⚔ MOCK MODE — generated by eva build --mock
155
+ // Restored automatically on next eva build (without --mock)
156
+ @EnableWebSecurity
157
+ @EnableMethodSecurity
158
+ @Configuration
159
+ public class SecurityConfig {
160
+ @Value("#{'${S}{cors.allowedOrigins}'.split(',')}")
161
+ private List<String> allowedOrigins;
162
+
163
+ @Value("#{'${S}{cors.allowedMethods}'.split(',')}")
164
+ private List<String> allowedMethods;
165
+
166
+ @Value("#{'${S}{cors.allowedHeaders}'.split(',')}")
167
+ private List<String> allowedHeaders;
168
+
169
+ private List<String> removeWhiteSpace(List<String> list) {
170
+ return list.stream().map(String::trim).toList();
171
+ }
172
+
173
+ @Bean
174
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
175
+ http
176
+ .csrf(csrf -> csrf.disable())
177
+ .cors(Customizer.withDefaults())
178
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
179
+ .authorizeHttpRequests(auth -> auth
180
+ .requestMatchers("/h2-console/**").permitAll()
181
+ .anyRequest().permitAll()
182
+ )
183
+ .headers(headers -> headers
184
+ .frameOptions(frame -> frame.sameOrigin())
185
+ );
186
+ return http.build();
187
+ }
188
+
189
+ @Bean
190
+ CorsConfigurationSource corsConfigurationSource() {
191
+ CorsConfiguration configuration = new CorsConfiguration();
192
+ configuration.setAllowedOrigins(removeWhiteSpace(allowedOrigins));
193
+ configuration.setAllowedMethods(removeWhiteSpace(allowedMethods));
194
+ configuration.setAllowedHeaders(removeWhiteSpace(allowedHeaders));
195
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
196
+ source.registerCorsConfiguration("/**", configuration);
197
+ return source;
198
+ }
199
+ }
200
+ `;
201
+ };
202
+
203
+ /**
204
+ * Backup original DB files and replace them with H2 config.
205
+ * Persists backups to .eva4j.json BEFORE writing any file so a
206
+ * crash mid-swap is recoverable on next run.
207
+ */
208
+ async function swapToH2(projectDir, packageName, configManager) {
209
+ const backups = {};
210
+
211
+ // ── db.yaml per environment ──────────────────────────────────────────────
212
+ for (const env of ENVS) {
213
+ const dbYamlPath = path.join(
214
+ projectDir, 'src', 'main', 'resources', 'parameters', env, 'db.yaml'
215
+ );
216
+ if (await fs.pathExists(dbYamlPath)) {
217
+ backups[`db_${env}`] = { path: dbYamlPath, content: await fs.readFile(dbYamlPath, 'utf-8') };
218
+ }
219
+ }
220
+
221
+ // ── build.gradle ─────────────────────────────────────────────────────────
222
+ const buildGradlePath = path.join(projectDir, 'build.gradle');
223
+ if (await fs.pathExists(buildGradlePath)) {
224
+ backups.buildGradle = { path: buildGradlePath, content: await fs.readFile(buildGradlePath, 'utf-8') };
225
+ }
226
+
227
+ // ── SecurityConfig.java ───────────────────────────────────────────────────
228
+ const packagePath = toPackagePath(packageName);
229
+ const securityConfigPath = path.join(
230
+ projectDir, 'src', 'main', 'java', packagePath,
231
+ 'shared', 'infrastructure', 'configurations', 'securityConfig', 'SecurityConfig.java'
232
+ );
233
+ if (await fs.pathExists(securityConfigPath)) {
234
+ backups.securityConfig = { path: securityConfigPath, content: await fs.readFile(securityConfigPath, 'utf-8') };
235
+ } else {
236
+ backups.securityConfig = { path: securityConfigPath, content: null };
237
+ }
238
+
239
+ // ── KafkaConfig.java — backup so it can be restored later ────────────────
240
+ const kafkaConfigPath = path.join(
241
+ projectDir, 'src', 'main', 'java', packagePath,
242
+ 'shared', 'infrastructure', 'configurations', 'kafkaConfig', 'KafkaConfig.java'
243
+ );
244
+ if (await fs.pathExists(kafkaConfigPath)) {
245
+ backups.kafkaConfig = { path: kafkaConfigPath, content: await fs.readFile(kafkaConfigPath, 'utf-8') };
246
+ }
247
+ // When Kafka is not installed, omit the key entirely — restoreFromH2() iterates
248
+ // Object.values(backups) and would crash trying to destructure a null entry.
249
+
250
+ // Persist backups BEFORE writing any file so a crash mid-swap is recoverable
251
+ await configManager.saveMockBackup(backups);
252
+
253
+ // ── Write H2 versions ────────────────────────────────────────────────────
254
+ for (const env of ENVS) {
255
+ if (backups[`db_${env}`]) {
256
+ await fs.writeFile(backups[`db_${env}`].path, H2_DB_YAML(packageName), 'utf-8');
257
+ }
258
+ }
259
+
260
+ if (backups.buildGradle) {
261
+ let swapped = backups.buildGradle.content.replace(
262
+ /^[ \t]*runtimeOnly\s+['"][^'"]+['"]\s*(?:\/\/.*)?$/m,
263
+ H2_GRADLE_LINE
264
+ );
265
+ // Remove spring-kafka dependencies block when Kafka is installed
266
+ swapped = swapped.replace(
267
+ /\n?[ \t]*\/\/ Kafka\n[ \t]*implementation 'org\.springframework\.kafka:spring-kafka'\n[ \t]*testImplementation 'org\.springframework\.kafka:spring-kafka-test'\n\n?[ \t]*/,
268
+ '\n\t'
269
+ );
270
+ await fs.writeFile(buildGradlePath, swapped, 'utf-8');
271
+ }
272
+
273
+ await fs.ensureDir(path.dirname(securityConfigPath));
274
+ await fs.writeFile(securityConfigPath, H2_SECURITY_CONFIG(packageName), 'utf-8');
275
+
276
+ // ── Remove KafkaConfig.java (restored from backup on eva build) ──────────
277
+ if (backups.kafkaConfig) {
278
+ await fs.remove(kafkaConfigPath);
279
+ }
280
+
281
+ return backups;
282
+ }
283
+
284
+ /**
285
+ * Backup and swap ONLY the broker layer (Kafka → Spring Event bus).
286
+ * Database config (db.yaml) and SecurityConfig.java are left untouched.
287
+ * Persists backups to .eva4j.json with _mockOnlyBroker = true.
288
+ */
289
+ async function swapBrokerOnly(projectDir, packageName, configManager) {
290
+ const backups = {};
291
+
292
+ // ── build.gradle ─────────────────────────────────────────────────────────
293
+ const buildGradlePath = path.join(projectDir, 'build.gradle');
294
+ if (await fs.pathExists(buildGradlePath)) {
295
+ backups.buildGradle = { path: buildGradlePath, content: await fs.readFile(buildGradlePath, 'utf-8') };
296
+ }
297
+
298
+ // ── KafkaConfig.java ──────────────────────────────────────────────────────
299
+ const packagePath = toPackagePath(packageName);
300
+ const kafkaConfigPath = path.join(
301
+ projectDir, 'src', 'main', 'java', packagePath,
302
+ 'shared', 'infrastructure', 'configurations', 'kafkaConfig', 'KafkaConfig.java'
303
+ );
304
+ if (await fs.pathExists(kafkaConfigPath)) {
305
+ backups.kafkaConfig = { path: kafkaConfigPath, content: await fs.readFile(kafkaConfigPath, 'utf-8') };
306
+ }
307
+
308
+ // Persist backups BEFORE writing any file so a crash mid-swap is recoverable
309
+ await configManager.saveMockBackup(backups, { onlyBroker: true });
310
+
311
+ // ── Remove spring-kafka from build.gradle (keep existing DB driver line) ──
312
+ if (backups.buildGradle) {
313
+ const swapped = backups.buildGradle.content.replace(
314
+ /\n?[ \t]*\/\/ Kafka\n[ \t]*implementation 'org\.springframework\.kafka:spring-kafka'\n[ \t]*testImplementation 'org\.springframework\.kafka:spring-kafka-test'\n\n?[ \t]*/,
315
+ '\n\t'
316
+ );
317
+ await fs.writeFile(buildGradlePath, swapped, 'utf-8');
318
+ }
319
+
320
+ // ── Remove KafkaConfig.java (restored from backup on eva build) ───────────
321
+ if (backups.kafkaConfig) {
322
+ await fs.remove(kafkaConfigPath);
323
+ }
324
+
325
+ return backups;
326
+ }
327
+
328
+ /**
329
+ * Restore all files from backup stored in .eva4j.json and clear the entry.
330
+ */
331
+ async function restoreFromH2(configManager) {
332
+ const backups = await configManager.popMockBackup();
333
+ if (!backups) return 0;
334
+
335
+ for (const { path: filePath, content } of Object.values(backups)) {
336
+ if (content === null) {
337
+ // File was created by mock — delete it on restore
338
+ await fs.remove(filePath);
339
+ } else {
340
+ await fs.writeFile(filePath, content, 'utf-8');
341
+ }
342
+ }
343
+ return Object.keys(backups).length;
344
+ }
345
+
346
+ // ── Main build command ──────────────────────────────────────────────────────────
347
+ async function buildCommand(options = {}) {
348
+ const projectDir = process.cwd();
349
+
350
+ // ── 1. Validate project ─────────────────────────────────────────────────────
351
+ if (!(await isEva4jProject(projectDir))) {
352
+ console.error(chalk.red('āŒ Not in an eva4j project directory'));
353
+ console.error(chalk.gray('Run this command inside a project created with eva4j'));
354
+ process.exit(1);
355
+ }
356
+
357
+ // ── 2. Load project config ──────────────────────────────────────────────────
358
+ const configManager = new ConfigManager(projectDir);
359
+ const projectConfig = await configManager.loadProjectConfig();
360
+
361
+ if (!projectConfig) {
362
+ console.error(chalk.red('āŒ Could not load project configuration'));
363
+ console.error(chalk.gray('Make sure .eva4j.json exists in the project root'));
364
+ process.exit(1);
365
+ }
366
+
367
+ // ── 2b. Restore mock config if a previous --mock run left files swapped ──────
368
+ if (!options.mock && await configManager.hasMockBackup()) {
369
+ const isOnlyBroker = await configManager.hasMockOnlyBroker();
370
+
371
+ if (isOnlyBroker) {
372
+ console.log(chalk.yellow('āš ļø Detected active --only-broker mock from a previous run.'));
373
+ console.log(chalk.yellow(' Restoring original broker configuration before continuing...\n'));
374
+ } else {
375
+ console.log(chalk.yellow('āš ļø Detected active mock (H2) config from a previous --mock run.'));
376
+ console.log(chalk.yellow(' Restoring original database configuration before continuing...\n'));
377
+ }
378
+
379
+ // Restore original files verbatim from backup (db.yaml, build.gradle, SecurityConfig, KafkaConfig)
380
+ // The backups contain the exact content the developer had configured — do NOT regenerate from
381
+ // projectConfig defaults, which only knows the DB type and not the custom URL/user/password.
382
+ await restoreFromH2(configManager);
383
+
384
+ if (!isOnlyBroker) {
385
+ // Re-apply the correct runtimeOnly DB driver line as a safety net for build.gradle
386
+ await regenerateBuildGradleDbDriver(projectDir, projectConfig);
387
+
388
+ // Force-regenerate SecurityConfig from the original template
389
+ const { packageName: pkgNameRestore } = projectConfig;
390
+ const pkgPathRestore = toPackagePath(pkgNameRestore);
391
+ const sharedBaseRestore = path.join(projectDir, 'src', 'main', 'java', pkgPathRestore, 'shared');
392
+ if (await fs.pathExists(sharedBaseRestore)) {
393
+ const sg = new SharedGenerator({
394
+ packageName: pkgNameRestore,
395
+ packagePath: pkgPathRestore,
396
+ projectName: projectConfig.projectName || projectConfig.artifactId,
397
+ groupId: projectConfig.groupId,
398
+ });
399
+ await sg.generateConfigurations(sharedBaseRestore);
400
+ }
401
+ }
402
+
403
+ console.log(chalk.green(' āœ… Configuration restored to original.\n'));
404
+ }
405
+
406
+ // ── MOCK swap — swap DB + broker config (or broker only), then run entity generation ─
407
+ if (options.mock) {
408
+ if (await configManager.hasMockBackup()) {
409
+ const alreadyOnlyBroker = await configManager.hasMockOnlyBroker();
410
+ const label = alreadyOnlyBroker ? 'broker-only mock' : 'mock (H2)';
411
+ console.log(chalk.yellow(`\n⚔ ${label} is already active. Run eva build without --mock to restore.\n`));
412
+ process.exit(0);
413
+ }
414
+
415
+ const { packageName: pkgName } = projectConfig;
416
+
417
+ if (options.onlyBroker) {
418
+ // ── BROKER-ONLY mode: keep database, replace broker ───────────────────
419
+ console.log(chalk.blue('\nšŸ”€ eva build --mock --only-broker\n'));
420
+ console.log(chalk.gray(` Project : ${projectConfig.projectName || projectConfig.artifactId}`));
421
+ console.log(chalk.yellow(' Mode : switching broker to Spring Event bus (database unchanged)\n'));
422
+ console.log(chalk.blue('━━━ Swapping broker config ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
423
+
424
+ const backups = await swapBrokerOnly(projectDir, pkgName, configManager);
425
+ const hasKafkaBackup = !!backups.kafkaConfig;
426
+ console.log(chalk.green(` āœ… ${Object.keys(backups).length} file(s) backed up and replaced`));
427
+ if (hasKafkaBackup) {
428
+ console.log(chalk.green(' āœ… KafkaConfig.java removed (Spring Events will be used instead)'));
429
+ console.log(chalk.green(' āœ… spring-kafka dependencies removed from build.gradle'));
430
+ }
431
+ console.log(chalk.gray(' Backup saved to .eva4j.json — will be restored on next eva build\n'));
432
+
433
+ // ── Regenerate broker layer if system.yaml exists and Kafka was installed
434
+ const systemDirBo = path.join(projectDir, 'system');
435
+ const systemYamlPathBo = path.join(systemDirBo, 'system.yaml');
436
+
437
+ if (hasKafkaBackup && (await fs.pathExists(systemYamlPathBo))) {
438
+ let systemConfigBo;
439
+ try {
440
+ const content = await fs.readFile(systemYamlPathBo, 'utf-8');
441
+ systemConfigBo = yaml.load(content);
442
+ } catch (err) {
443
+ console.error(chalk.red('āŒ Failed to parse system/system.yaml:'), err.message);
444
+ process.exit(1);
445
+ }
446
+
447
+ const { modules: mockModulesBo = [] } = systemConfigBo;
448
+ const pkgPathBo = toPackagePath(pkgName);
449
+
450
+ if (mockModulesBo.length > 0) {
451
+ console.log(chalk.blue('━━━ Regenerating broker layer (mock) ━━━━━━━━━━━━━━━━━━━━━━━━'));
452
+
453
+ for (const mod of mockModulesBo) {
454
+ const sourceYaml = path.join(systemDirBo, `${mod.name}.yaml`);
455
+ const modulePackageName = toCamelCase(mod.name);
456
+ const destYaml = path.join(projectDir, 'src', 'main', 'java', pkgPathBo, modulePackageName, 'domain.yaml');
457
+ if (!(await fs.pathExists(sourceYaml))) {
458
+ console.log(chalk.yellow(` āš ļø system/${mod.name}.yaml not found — skipping ${mod.name}`));
459
+ continue;
460
+ }
461
+ const content = await fs.readFile(sourceYaml, 'utf-8');
462
+ await fs.ensureDir(path.dirname(destYaml));
463
+ await fs.writeFile(destYaml, content, 'utf-8');
464
+ console.log(chalk.green(` āœ… ${mod.name}/domain.yaml updated`));
465
+ }
466
+
467
+ for (const mod of mockModulesBo) {
468
+ const modulePackageName = toCamelCase(mod.name);
469
+ const domainYamlPath = path.join(projectDir, 'src', 'main', 'java', pkgPathBo, modulePackageName, 'domain.yaml');
470
+ if (!(await fs.pathExists(domainYamlPath))) {
471
+ console.log(chalk.yellow(` āš ļø domain.yaml not found for '${mod.name}' — skipping`));
472
+ continue;
473
+ }
474
+ console.log(chalk.cyan(`\n Regenerating broker layer for: ${mod.name}`));
475
+ await generateEntitiesCommand(mod.name, { force: false, brokerMode: 'mock' });
476
+ }
477
+ }
478
+ } else if (hasKafkaBackup) {
479
+ console.log(chalk.yellow(' ā„¹ļø No system/system.yaml found — broker files must be regenerated manually.'));
480
+ console.log(chalk.yellow(' Run: eva g entities <module> (with --force if needed)'));
481
+ }
482
+
483
+ console.log();
484
+ console.log(chalk.yellow(' ⚔ Broker-only mock active. Database config unchanged.'));
485
+ console.log(chalk.yellow(' Run ./gradlew bootRun to start.'));
486
+ console.log(chalk.yellow(' Run eva build (without --mock) to restore the original broker config.\n'));
487
+ return;
488
+ }
489
+
490
+ // ── FULL mock mode: DB → H2 + broker → Spring Events ─────────────────────
491
+ console.log(chalk.blue('\nšŸ”€ eva build --mock\n'));
492
+ console.log(chalk.gray(` Project : ${projectConfig.projectName || projectConfig.artifactId}`));
493
+ console.log(chalk.yellow(' Mode : switching to H2 in-memory database + Spring Event bus\n'));
494
+ console.log(chalk.blue('━━━ Swapping database & broker config ━━━━━━━━━━━━━━━━━━━━━━━━'));
495
+
496
+ const backups = await swapToH2(projectDir, pkgName, configManager);
497
+ const hasKafkaBackup = !!backups.kafkaConfig;
498
+ console.log(chalk.green(` āœ… ${Object.keys(backups).length} file(s) backed up and replaced`));
499
+ if (hasKafkaBackup) {
500
+ console.log(chalk.green(' āœ… KafkaConfig.java removed (Spring Events will be used instead)'));
501
+ console.log(chalk.green(' āœ… spring-kafka dependencies removed from build.gradle'));
502
+ }
503
+ console.log(chalk.gray(' Backup saved to .eva4j.json — will be restored on next eva build\n'));
504
+
505
+ // ── Regenerate broker layer if system.yaml exists and Kafka was installed ──
506
+ const systemDir = path.join(projectDir, 'system');
507
+ const systemYamlPath = path.join(systemDir, 'system.yaml');
508
+
509
+ if (hasKafkaBackup && (await fs.pathExists(systemYamlPath))) {
510
+ let systemConfig;
511
+ try {
512
+ const content = await fs.readFile(systemYamlPath, 'utf-8');
513
+ systemConfig = yaml.load(content);
514
+ } catch (err) {
515
+ console.error(chalk.red('āŒ Failed to parse system/system.yaml:'), err.message);
516
+ process.exit(1);
517
+ }
518
+
519
+ const { modules: mockModules = [] } = systemConfig;
520
+ const pkgPath = toPackagePath(pkgName);
521
+
522
+ if (mockModules.length > 0) {
523
+ console.log(chalk.blue('━━━ Regenerating broker layer (mock) ━━━━━━━━━━━━━━━━━━━━━━━━'));
524
+
525
+ // Step 3: Copy domain.yaml files
526
+ for (const mod of mockModules) {
527
+ const sourceYaml = path.join(systemDir, `${mod.name}.yaml`);
528
+ const modulePackageName = toCamelCase(mod.name);
529
+ const destYaml = path.join(projectDir, 'src', 'main', 'java', pkgPath, modulePackageName, 'domain.yaml');
530
+ if (!(await fs.pathExists(sourceYaml))) {
531
+ console.log(chalk.yellow(` āš ļø system/${mod.name}.yaml not found — skipping ${mod.name}`));
532
+ continue;
533
+ }
534
+ const content = await fs.readFile(sourceYaml, 'utf-8');
535
+ await fs.ensureDir(path.dirname(destYaml));
536
+ await fs.writeFile(destYaml, content, 'utf-8');
537
+ console.log(chalk.green(` āœ… ${mod.name}/domain.yaml updated`));
538
+ }
539
+
540
+ // Step 4: Regenerate entities with mock broker
541
+ for (const mod of mockModules) {
542
+ const modulePackageName = toCamelCase(mod.name);
543
+ const domainYamlPath = path.join(projectDir, 'src', 'main', 'java', pkgPath, modulePackageName, 'domain.yaml');
544
+ if (!(await fs.pathExists(domainYamlPath))) {
545
+ console.log(chalk.yellow(` āš ļø domain.yaml not found for '${mod.name}' — skipping`));
546
+ continue;
547
+ }
548
+ console.log(chalk.cyan(`\n Regenerating broker layer for: ${mod.name}`));
549
+ await generateEntitiesCommand(mod.name, { force: false, brokerMode: 'mock' });
550
+ }
551
+ }
552
+ } else if (hasKafkaBackup) {
553
+ console.log(chalk.yellow(' ā„¹ļø No system/system.yaml found — broker files must be regenerated manually.'));
554
+ console.log(chalk.yellow(' Run: eva g entities <module> (with --force if needed)'));
555
+ }
556
+
557
+ console.log();
558
+ console.log(chalk.yellow(' ⚔ Mock mode active. Run ./gradlew bootRun to start.'));
559
+ console.log(chalk.yellow(' Run eva build (without --mock) to restore the original config.\n'));
560
+ return;
561
+ }
562
+
563
+ const { packageName } = projectConfig;
564
+ const packagePath = toPackagePath(packageName);
565
+
566
+ // ── 3. Read system/system.yaml ──────────────────────────────────────────────
567
+ const systemDir = path.join(projectDir, 'system');
568
+ const systemYamlPath = path.join(systemDir, 'system.yaml');
569
+
570
+ if (!(await fs.pathExists(systemYamlPath))) {
571
+ console.error(chalk.red('āŒ system/system.yaml not found'));
572
+ console.error(chalk.gray('Create system/system.yaml first with module definitions'));
573
+ process.exit(1);
574
+ }
575
+
576
+ let systemConfig;
577
+ try {
578
+ const content = await fs.readFile(systemYamlPath, 'utf-8');
579
+ systemConfig = yaml.load(content);
580
+ } catch (err) {
581
+ console.error(chalk.red('āŒ Failed to parse system/system.yaml:'), err.message);
582
+ process.exit(1);
583
+ }
584
+
585
+ const { modules = [], messaging } = systemConfig;
586
+
587
+ if (!modules.length) {
588
+ console.log(chalk.yellow('āš ļø No modules defined in system/system.yaml'));
589
+ process.exit(0);
590
+ }
591
+
592
+ console.log(chalk.blue('\nšŸ—ļø eva build\n'));
593
+ console.log(chalk.gray(` Project : ${projectConfig.projectName || projectConfig.artifactId}`));
594
+ console.log(chalk.gray(` Modules : ${modules.map(m => m.name).join(', ')}`));
595
+ console.log();
596
+
597
+ // ── STEP 1: Create modules ───────────────────────────────────────────────
598
+ console.log(chalk.blue('━━━ Step 1: Creating modules ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
599
+
600
+ for (const mod of modules) {
601
+ const modulePackageName = toCamelCase(mod.name);
602
+
603
+ if (await configManager.moduleExists(modulePackageName)) {
604
+ console.log(chalk.gray(` ā­ ${mod.name} — already exists, skipping`));
605
+ continue;
606
+ }
607
+
608
+ const moduleDir = path.join(projectDir, 'src', 'main', 'java', packagePath, modulePackageName);
609
+ if (await fs.pathExists(moduleDir)) {
610
+ console.log(chalk.gray(` ā­ ${mod.name} — directory already exists, skipping`));
611
+ continue;
612
+ }
613
+
614
+ console.log(chalk.cyan(` āž• Adding module: ${mod.name}`));
615
+ await addModuleCommand(mod.name, {});
616
+ await configManager.loadProjectConfig();
617
+ }
618
+
619
+ console.log();
620
+
621
+ // ── STEP 2: Install broker client ────────────────────────────────────────
622
+ console.log(chalk.blue('━━━ Step 2: Installing broker client ━━━━━━━━━━━━━━━━━━━━━━━━━'));
623
+
624
+ const brokerEnabled = messaging && messaging.enabled === true;
625
+ const broker = messaging && messaging.broker;
626
+
627
+ if (!brokerEnabled || !broker) {
628
+ console.log(chalk.gray(' ā­ No messaging configured, skipping broker install'));
629
+ } else if (broker === 'kafka') {
630
+ if (await configManager.featureExists('kafka')) {
631
+ console.log(chalk.gray(' ā­ kafka-client — already installed, skipping'));
632
+ } else {
633
+ console.log(chalk.cyan(' āž• Installing kafka-client'));
634
+ await addKafkaClientCommand();
635
+ }
636
+ } else {
637
+ console.log(chalk.yellow(` āš ļø Broker '${broker}' is not supported by eva build (only kafka is supported)`));
638
+ }
639
+
640
+ console.log();
641
+
642
+ // ── STEP 3: Copy domain.yaml files ──────────────────────────────────────
643
+ console.log(chalk.blue('━━━ Step 3: Copying domain.yaml files ━━━━━━━━━━━━━━━━━━━━━━━'));
644
+
645
+ for (const mod of modules) {
646
+ const sourceYaml = path.join(systemDir, `${mod.name}.yaml`);
647
+ const modulePackageName = toCamelCase(mod.name);
648
+ const destYaml = path.join(
649
+ projectDir, 'src', 'main', 'java', packagePath, modulePackageName, 'domain.yaml'
650
+ );
651
+
652
+ if (!(await fs.pathExists(sourceYaml))) {
653
+ console.log(chalk.yellow(` āš ļø system/${mod.name}.yaml not found — skipping ${mod.name}`));
654
+ continue;
655
+ }
656
+
657
+ const content = await fs.readFile(sourceYaml, 'utf-8');
658
+ await fs.ensureDir(path.dirname(destYaml));
659
+ await fs.writeFile(destYaml, content, 'utf-8');
660
+ console.log(chalk.green(` āœ… ${mod.name}/domain.yaml updated`));
661
+ }
662
+
663
+ console.log();
664
+
665
+ // ── STEP 4: Generate entities ────────────────────────────────────────────
666
+ console.log(chalk.blue('━━━ Step 4: Generating entities ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
667
+
668
+ const generateOptions = { force: options.force || false };
669
+
670
+ for (const mod of modules) {
671
+ const modulePackageName = toCamelCase(mod.name);
672
+ const domainYamlPath = path.join(
673
+ projectDir, 'src', 'main', 'java', packagePath, modulePackageName, 'domain.yaml'
674
+ );
675
+
676
+ if (!(await fs.pathExists(domainYamlPath))) {
677
+ console.log(chalk.yellow(` āš ļø domain.yaml not found for '${mod.name}' — skipping entity generation`));
678
+ continue;
679
+ }
680
+
681
+ console.log(chalk.cyan(`\n Generating entities for: ${mod.name}`));
682
+ await generateEntitiesCommand(mod.name, { ...generateOptions, skipPostman: true });
683
+ }
684
+
685
+ console.log();
686
+
687
+ // ── STEP 5: Generate unified Postman collection ─────────────────────────
688
+ console.log(chalk.blue('━━━ Step 5: Generating unified Postman collection ━━━━━━━━━━━━'));
689
+
690
+ try {
691
+ const collectionPath = await generateUnifiedPostmanCollection({
692
+ projectDir,
693
+ systemDir,
694
+ packageName,
695
+ systemConfig,
696
+ projectConfig,
697
+ });
698
+
699
+ if (collectionPath) {
700
+ const relPath = path.relative(projectDir, collectionPath);
701
+ console.log(chalk.green(` āœ… ${relPath}`));
702
+ console.log(chalk.cyan('\n šŸ’” Import this collection into Postman to test all your API endpoints!'));
703
+ } else {
704
+ console.log(chalk.gray(' ā­ No modules with domain definitions found — skipping'));
705
+ }
706
+ } catch (err) {
707
+ console.log(chalk.yellow(` āš ļø Could not generate Postman collection: ${err.message}`));
708
+ }
709
+
710
+ console.log();
711
+ console.log(chalk.green('āœ… eva build completed successfully\n'));
712
+ }
713
+
714
+ module.exports = buildCommand;