@devstroupe/devkit-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +40 -0
  2. package/dist/boilerplates/angular-template/.dockerignore +4 -0
  3. package/dist/boilerplates/angular-template/.postcssrc.json +5 -0
  4. package/dist/boilerplates/angular-template/Dockerfile +14 -0
  5. package/dist/boilerplates/angular-template/angular.json +85 -0
  6. package/dist/boilerplates/angular-template/components.json +5 -0
  7. package/dist/boilerplates/angular-template/framework.code-workspace +7 -0
  8. package/dist/boilerplates/angular-template/nginx.conf +23 -0
  9. package/dist/boilerplates/angular-template/node_modules/.bin/browserslist +21 -0
  10. package/dist/boilerplates/angular-template/node_modules/.bin/jiti +21 -0
  11. package/dist/boilerplates/angular-template/node_modules/.bin/lessc +21 -0
  12. package/dist/boilerplates/angular-template/node_modules/.bin/ng +21 -0
  13. package/dist/boilerplates/angular-template/node_modules/.bin/ng-xi18n +21 -0
  14. package/dist/boilerplates/angular-template/node_modules/.bin/ngc +21 -0
  15. package/dist/boilerplates/angular-template/node_modules/.bin/sass +21 -0
  16. package/dist/boilerplates/angular-template/node_modules/.bin/terser +21 -0
  17. package/dist/boilerplates/angular-template/node_modules/.bin/tsc +21 -0
  18. package/dist/boilerplates/angular-template/node_modules/.bin/tsserver +21 -0
  19. package/dist/boilerplates/angular-template/node_modules/.bin/vite +21 -0
  20. package/dist/boilerplates/angular-template/node_modules/.bin/vitest +21 -0
  21. package/dist/boilerplates/angular-template/node_modules/.bin/yaml +21 -0
  22. package/dist/boilerplates/angular-template/package.json +47 -0
  23. package/dist/boilerplates/angular-template/postcss.config.js +5 -0
  24. package/dist/boilerplates/angular-template/proxy.conf.json +7 -0
  25. package/dist/boilerplates/angular-template/src/app/app.component.html +3 -0
  26. package/dist/boilerplates/angular-template/src/app/app.component.ts +12 -0
  27. package/dist/boilerplates/angular-template/src/app/app.config.ts +59 -0
  28. package/dist/boilerplates/angular-template/src/app/app.entity-routes.ts +4 -0
  29. package/dist/boilerplates/angular-template/src/app/app.routes.ts +25 -0
  30. package/dist/boilerplates/angular-template/src/app/core/guards/auth.guard.ts +16 -0
  31. package/dist/boilerplates/angular-template/src/app/core/services/auth.service.ts +98 -0
  32. package/dist/boilerplates/angular-template/src/app/modules/auth/login/login.component.html +51 -0
  33. package/dist/boilerplates/angular-template/src/app/modules/auth/login/login.component.ts +47 -0
  34. package/dist/boilerplates/angular-template/src/app/modules/auth/register/register.component.html +59 -0
  35. package/dist/boilerplates/angular-template/src/app/modules/auth/register/register.component.ts +47 -0
  36. package/dist/boilerplates/angular-template/src/app/modules/dashboard/dashboard.component.html +14 -0
  37. package/dist/boilerplates/angular-template/src/app/modules/dashboard/dashboard.component.ts +23 -0
  38. package/dist/boilerplates/angular-template/src/app/shared/interceptors/tenant.interceptor.ts +25 -0
  39. package/dist/boilerplates/angular-template/src/index.html +64 -0
  40. package/dist/boilerplates/angular-template/src/main.ts +6 -0
  41. package/dist/boilerplates/angular-template/src/styles.css +9 -0
  42. package/dist/boilerplates/angular-template/tsconfig.json +36 -0
  43. package/dist/boilerplates/nest-template/Dockerfile +16 -0
  44. package/dist/boilerplates/nest-template/node_modules/.bin/acorn +21 -0
  45. package/dist/boilerplates/nest-template/node_modules/.bin/nest +21 -0
  46. package/dist/boilerplates/nest-template/node_modules/.bin/prettier +21 -0
  47. package/dist/boilerplates/nest-template/node_modules/.bin/tsc +21 -0
  48. package/dist/boilerplates/nest-template/node_modules/.bin/tsserver +21 -0
  49. package/dist/boilerplates/nest-template/node_modules/.bin/typeorm +21 -0
  50. package/dist/boilerplates/nest-template/node_modules/.bin/typeorm-ts-node-commonjs +21 -0
  51. package/dist/boilerplates/nest-template/node_modules/.bin/typeorm-ts-node-esm +21 -0
  52. package/dist/boilerplates/nest-template/node_modules/.bin/webpack +21 -0
  53. package/dist/boilerplates/nest-template/package.json +43 -0
  54. package/dist/boilerplates/nest-template/src/app.module.ts +24 -0
  55. package/dist/boilerplates/nest-template/src/database/data-source.ts +16 -0
  56. package/dist/boilerplates/nest-template/src/database/migrations/1700000000000-create-users.ts +26 -0
  57. package/dist/boilerplates/nest-template/src/main.ts +46 -0
  58. package/dist/boilerplates/nest-template/src/modules/auth/auth.controller.ts +34 -0
  59. package/dist/boilerplates/nest-template/src/modules/auth/auth.module.ts +22 -0
  60. package/dist/boilerplates/nest-template/src/modules/auth/auth.service.ts +57 -0
  61. package/dist/boilerplates/nest-template/src/modules/auth/current-user.decorator.ts +8 -0
  62. package/dist/boilerplates/nest-template/src/modules/auth/jwt-auth.guard.ts +31 -0
  63. package/dist/boilerplates/nest-template/src/modules/auth/roles.decorator.ts +4 -0
  64. package/dist/boilerplates/nest-template/src/modules/auth/roles.guard.ts +24 -0
  65. package/dist/boilerplates/nest-template/src/modules/user/database-seed.service.ts +47 -0
  66. package/dist/boilerplates/nest-template/src/modules/user/domain/user.repository.ts +9 -0
  67. package/dist/boilerplates/nest-template/src/modules/user/domain/user.ts +9 -0
  68. package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.orm-entity.ts +31 -0
  69. package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.repository.adapter.ts +53 -0
  70. package/dist/boilerplates/nest-template/src/modules/user/service/user.service.ts +44 -0
  71. package/dist/boilerplates/nest-template/src/modules/user/user.module.ts +20 -0
  72. package/dist/boilerplates/nest-template/tsconfig.build.json +4 -0
  73. package/dist/boilerplates/nest-template/tsconfig.json +21 -0
  74. package/dist/index.d.ts +2 -0
  75. package/dist/index.js +1967 -0
  76. package/dist/migrations/sort-entities.d.ts +2 -0
  77. package/dist/migrations/sort-entities.js +30 -0
  78. package/dist/rules/linter-rules.d.ts +12 -0
  79. package/dist/rules/linter-rules.js +338 -0
  80. package/dist/rules/linter-rules.test.d.ts +1 -0
  81. package/dist/rules/linter-rules.test.js +214 -0
  82. package/dist/templates/angular/dialog-handler.template.d.ts +2 -0
  83. package/dist/templates/angular/dialog-handler.template.js +49 -0
  84. package/dist/templates/angular/form.template.d.ts +3 -0
  85. package/dist/templates/angular/form.template.js +453 -0
  86. package/dist/templates/angular/index.d.ts +4 -0
  87. package/dist/templates/angular/index.js +20 -0
  88. package/dist/templates/angular/list.template.d.ts +3 -0
  89. package/dist/templates/angular/list.template.js +213 -0
  90. package/dist/templates/angular/service.template.d.ts +1 -0
  91. package/dist/templates/angular/service.template.js +20 -0
  92. package/dist/templates/cli-templates.d.ts +2 -0
  93. package/dist/templates/cli-templates.js +18 -0
  94. package/dist/templates/nest/crud.templates.d.ts +9 -0
  95. package/dist/templates/nest/crud.templates.js +362 -0
  96. package/dist/templates/nest/index.d.ts +3 -0
  97. package/dist/templates/nest/index.js +19 -0
  98. package/dist/templates/nest/microservice.templates.d.ts +4 -0
  99. package/dist/templates/nest/microservice.templates.js +157 -0
  100. package/dist/templates/nest/migration.template.d.ts +3 -0
  101. package/dist/templates/nest/migration.template.js +127 -0
  102. package/dist/templates/relationship-templates.test.d.ts +1 -0
  103. package/dist/templates/relationship-templates.test.js +181 -0
  104. package/dist/templates/shared/names.d.ts +3 -0
  105. package/dist/templates/shared/names.js +14 -0
  106. package/dist/templates/shared/relationships.d.ts +27 -0
  107. package/dist/templates/shared/relationships.js +96 -0
  108. package/dist/templates/ui/components/avatar.template.d.ts +3 -0
  109. package/dist/templates/ui/components/avatar.template.js +66 -0
  110. package/dist/templates/ui/components/badge.template.d.ts +3 -0
  111. package/dist/templates/ui/components/badge.template.js +27 -0
  112. package/dist/templates/ui/components/button.template.d.ts +5 -0
  113. package/dist/templates/ui/components/button.template.js +64 -0
  114. package/dist/templates/ui/components/card-list.template.d.ts +5 -0
  115. package/dist/templates/ui/components/card-list.template.js +79 -0
  116. package/dist/templates/ui/components/dialog.template.d.ts +5 -0
  117. package/dist/templates/ui/components/dialog.template.js +51 -0
  118. package/dist/templates/ui/components/filter.template.d.ts +4 -0
  119. package/dist/templates/ui/components/filter.template.js +218 -0
  120. package/dist/templates/ui/components/index.d.ts +10 -0
  121. package/dist/templates/ui/components/index.js +26 -0
  122. package/dist/templates/ui/components/input.template.d.ts +4 -0
  123. package/dist/templates/ui/components/input.template.js +76 -0
  124. package/dist/templates/ui/components/select.template.d.ts +4 -0
  125. package/dist/templates/ui/components/select.template.js +176 -0
  126. package/dist/templates/ui/components/simple-list.template.d.ts +5 -0
  127. package/dist/templates/ui/components/simple-list.template.js +89 -0
  128. package/dist/templates/ui/components/table.template.d.ts +5 -0
  129. package/dist/templates/ui/components/table.template.js +112 -0
  130. package/dist/templates/ui/index.d.ts +9 -0
  131. package/dist/templates/ui/index.js +25 -0
  132. package/dist/templates/ui/layout/header.template.d.ts +5 -0
  133. package/dist/templates/ui/layout/header.template.js +236 -0
  134. package/dist/templates/ui/layout/index.d.ts +4 -0
  135. package/dist/templates/ui/layout/index.js +20 -0
  136. package/dist/templates/ui/layout/main-layout.template.d.ts +5 -0
  137. package/dist/templates/ui/layout/main-layout.template.js +126 -0
  138. package/dist/templates/ui/layout/shell.template.d.ts +5 -0
  139. package/dist/templates/ui/layout/shell.template.js +76 -0
  140. package/dist/templates/ui/layout/sidebar.template.d.ts +5 -0
  141. package/dist/templates/ui/layout/sidebar.template.js +412 -0
  142. package/dist/templates/ui/playground.template.d.ts +5 -0
  143. package/dist/templates/ui/playground.template.js +570 -0
  144. package/dist/templates/ui/readme.template.d.ts +1 -0
  145. package/dist/templates/ui/readme.template.js +23 -0
  146. package/dist/templates/ui/shared/colors.d.ts +1 -0
  147. package/dist/templates/ui/shared/colors.js +31 -0
  148. package/dist/templates/ui/shared/profile-component.template.d.ts +5 -0
  149. package/dist/templates/ui/shared/profile-component.template.js +403 -0
  150. package/dist/templates/ui/shared/profile-service.template.d.ts +1 -0
  151. package/dist/templates/ui/shared/profile-service.template.js +44 -0
  152. package/dist/templates/ui/shared/theme-service.template.d.ts +1 -0
  153. package/dist/templates/ui/shared/theme-service.template.js +47 -0
  154. package/dist/templates/ui/spartan/badge-directive.template.d.ts +3 -0
  155. package/dist/templates/ui/spartan/badge-directive.template.js +50 -0
  156. package/dist/templates/ui/spartan/button-directive.template.d.ts +3 -0
  157. package/dist/templates/ui/spartan/button-directive.template.js +82 -0
  158. package/dist/templates/ui/spartan/button-token.template.d.ts +3 -0
  159. package/dist/templates/ui/spartan/button-token.template.js +31 -0
  160. package/dist/templates/ui/spartan/hlm-core.template.d.ts +3 -0
  161. package/dist/templates/ui/spartan/hlm-core.template.js +322 -0
  162. package/dist/templates/ui/spartan/index.d.ts +5 -0
  163. package/dist/templates/ui/spartan/index.js +21 -0
  164. package/dist/templates/ui/spartan/input-directive.template.d.ts +3 -0
  165. package/dist/templates/ui/spartan/input-directive.template.js +31 -0
  166. package/dist/templates/ui/theme-css.template.d.ts +7 -0
  167. package/dist/templates/ui/theme-css.template.js +210 -0
  168. package/dist/templates/ui-templates.d.ts +1 -0
  169. package/dist/templates/ui-templates.js +17 -0
  170. package/package.json +50 -0
package/dist/index.js ADDED
@@ -0,0 +1,1967 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const commander_1 = require("commander");
38
+ const path = __importStar(require("path"));
39
+ const fs = __importStar(require("fs-extra"));
40
+ const cli_templates_1 = require("./templates/cli-templates");
41
+ const ui_templates_1 = require("./templates/ui-templates");
42
+ const linter_rules_1 = require("./rules/linter-rules");
43
+ const relationships_1 = require("./templates/shared/relationships");
44
+ const sort_entities_1 = require("./migrations/sort-entities");
45
+ const child_process_1 = require("child_process");
46
+ // Registra ts-node para poder carregar arquivos .ts (como o config)
47
+ try {
48
+ require('ts-node').register({
49
+ compilerOptions: { module: 'commonjs' }
50
+ });
51
+ }
52
+ catch (e) {
53
+ // Silencioso se ts-node não estiver presente localmente
54
+ }
55
+ const program = new commander_1.Command();
56
+ const SPARTAN_STYLES = ['nova', 'vega', 'lyra', 'maia', 'mira', 'luma'];
57
+ function isSpartanStyle(value) {
58
+ return SPARTAN_STYLES.includes(value);
59
+ }
60
+ function getDesignSystemConfig(config) {
61
+ return config?.designSystem || {};
62
+ }
63
+ function assertValidRelationshipConfig(config) {
64
+ (0, relationships_1.validateRelationships)(config.entities ?? []);
65
+ if (config.backendType !== 'microservices')
66
+ return;
67
+ const manyToMany = (config.entities ?? []).flatMap((item) => (0, relationships_1.getRelationships)(item.properties ?? []).filter((relation) => relation.kind === 'many-to-many'));
68
+ if (manyToMany.length > 0) {
69
+ throw new Error('Many-to-many relationships require a dedicated junction CRUD when backendType is "microservices".');
70
+ }
71
+ }
72
+ function ensureMigrationInfrastructure(backendRoot) {
73
+ const databaseDir = path.join(backendRoot, 'src', 'database');
74
+ fs.ensureDirSync(path.join(databaseDir, 'migrations'));
75
+ const dataSourcePath = path.join(databaseDir, 'data-source.ts');
76
+ if (!fs.existsSync(dataSourcePath)) {
77
+ fs.writeFileSync(dataSourcePath, (0, cli_templates_1.nestDataSourceTemplate)(), 'utf-8');
78
+ }
79
+ const packagePath = path.join(backendRoot, 'package.json');
80
+ if (fs.existsSync(packagePath)) {
81
+ const packageJson = fs.readJsonSync(packagePath);
82
+ packageJson.scripts = {
83
+ ...(packageJson.scripts ?? {}),
84
+ 'migration:run': 'typeorm-ts-node-commonjs -d src/database/data-source.ts migration:run',
85
+ 'migration:revert': 'typeorm-ts-node-commonjs -d src/database/data-source.ts migration:revert',
86
+ 'migration:generate': 'typeorm-ts-node-commonjs -d src/database/data-source.ts migration:generate src/database/migrations/schema',
87
+ };
88
+ packageJson.devDependencies = { ...(packageJson.devDependencies ?? {}), 'ts-node': '^10.9.2' };
89
+ packageJson.dependencies = { ...(packageJson.dependencies ?? {}), dotenv: '^16.4.5' };
90
+ fs.writeJsonSync(packagePath, packageJson, { spaces: 2 });
91
+ }
92
+ const appModulePath = path.join(backendRoot, 'src', 'app.module.ts');
93
+ if (fs.existsSync(appModulePath)) {
94
+ const appModule = fs.readFileSync(appModulePath, 'utf-8');
95
+ const updated = appModule.replace(/synchronize:\s*true/g, 'synchronize: false');
96
+ if (updated !== appModule)
97
+ fs.writeFileSync(appModulePath, updated, 'utf-8');
98
+ }
99
+ }
100
+ function ensureInitialMigrations(backendRoot, entities, propertyOverrides = new Map()) {
101
+ ensureMigrationInfrastructure(backendRoot);
102
+ const migrationsDir = path.join(backendRoot, 'src', 'database', 'migrations');
103
+ const existing = fs.readdirSync(migrationsDir);
104
+ const missing = (0, sort_entities_1.sortEntitiesForMigrations)(entities).filter((entity) => !existing.some((file) => file.endsWith(`-create-${entity.name}.ts`)));
105
+ const baseTimestamp = Date.now();
106
+ missing.forEach((entity, index) => {
107
+ const timestamp = baseTimestamp + index;
108
+ const fileName = `${timestamp}-create-${entity.name}.ts`;
109
+ const properties = propertyOverrides.get(entity.name) ?? entity.properties;
110
+ fs.writeFileSync(path.join(migrationsDir, fileName), (0, cli_templates_1.nestMigrationTemplate)(entity, entities, timestamp, properties), 'utf-8');
111
+ console.log(`[Migration] Criada ${fileName}`);
112
+ });
113
+ }
114
+ function getSpartanStyle(config) {
115
+ const designSystem = getDesignSystemConfig(config);
116
+ const configuredStyle = designSystem.spartanStyle || designSystem.style || designSystem.preset || 'vega';
117
+ return isSpartanStyle(configuredStyle) ? configuredStyle : 'vega';
118
+ }
119
+ function getCurrentSpartanStyle(frontendRoot, config) {
120
+ const componentsPath = path.join(frontendRoot, 'components.json');
121
+ if (fs.existsSync(componentsPath)) {
122
+ try {
123
+ const componentsConfig = fs.readJsonSync(componentsPath);
124
+ if (isSpartanStyle(componentsConfig.style)) {
125
+ return componentsConfig.style;
126
+ }
127
+ }
128
+ catch {
129
+ // fallback para o devstroupe.config.ts abaixo
130
+ }
131
+ }
132
+ return getSpartanStyle(config);
133
+ }
134
+ function getPackageManagerCommand(projectRoot) {
135
+ const localNg = path.join(projectRoot, 'node_modules', '.bin', process.platform === 'win32' ? 'ng.cmd' : 'ng');
136
+ if (fs.existsSync(localNg)) {
137
+ return `"${localNg}"`;
138
+ }
139
+ const pkgPath = path.join(projectRoot, 'package.json');
140
+ try {
141
+ const pkg = fs.existsSync(pkgPath) ? fs.readJsonSync(pkgPath) : {};
142
+ if (typeof pkg.packageManager === 'string' && pkg.packageManager.startsWith('pnpm')) {
143
+ return 'pnpm exec ng';
144
+ }
145
+ }
146
+ catch {
147
+ // fallback abaixo
148
+ }
149
+ if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
150
+ return 'pnpm exec ng';
151
+ }
152
+ return 'npx ng';
153
+ }
154
+ function ensureSpartanComponentsJson(frontendRoot, style) {
155
+ const componentsPath = path.join(frontendRoot, 'components.json');
156
+ const nextConfig = {
157
+ componentsPath: 'libs/ui',
158
+ importAlias: '@spartan-ng/helm',
159
+ style
160
+ };
161
+ fs.writeJsonSync(componentsPath, nextConfig, { spaces: 2 });
162
+ console.log(`[Spartan] components.json configurado com style="${style}".`);
163
+ }
164
+ function ensureSpartanTsconfigPaths(frontendRoot) {
165
+ const tsconfigPath = path.join(frontendRoot, 'tsconfig.json');
166
+ if (!fs.existsSync(tsconfigPath))
167
+ return;
168
+ const utilsIndexPath = path.join(frontendRoot, 'libs', 'ui', 'utils', 'src', 'index.ts');
169
+ if (!fs.existsSync(utilsIndexPath)) {
170
+ throw new Error('[Spartan] libs/ui/utils não foi gerado. Execute o generator do Spartan antes de registrar os aliases.');
171
+ }
172
+ const tsconfig = fs.readJsonSync(tsconfigPath);
173
+ tsconfig.compilerOptions = tsconfig.compilerOptions || {};
174
+ tsconfig.compilerOptions.paths = tsconfig.compilerOptions.paths || {};
175
+ tsconfig.compilerOptions.paths['@spartan-ng/helm/utils'] = ['libs/ui/utils/src/index.ts'];
176
+ tsconfig.compilerOptions.paths['@spartan-ng/helm/*'] = ['libs/ui/*/src/index.ts'];
177
+ fs.writeJsonSync(tsconfigPath, tsconfig, { spaces: 2 });
178
+ console.log('[Spartan] Path aliases @spartan-ng/helm configurados no tsconfig.json.');
179
+ }
180
+ function removeStaleSpartanTsconfigPaths(frontendRoot) {
181
+ const tsconfigPath = path.join(frontendRoot, 'tsconfig.json');
182
+ if (!fs.existsSync(tsconfigPath))
183
+ return;
184
+ const tsconfig = fs.readJsonSync(tsconfigPath);
185
+ const paths = tsconfig.compilerOptions?.paths;
186
+ if (!paths)
187
+ return;
188
+ const utilsIndexPath = path.join(frontendRoot, 'libs', 'ui', 'utils', 'src', 'index.ts');
189
+ let updated = false;
190
+ if (!fs.existsSync(utilsIndexPath) && paths['@spartan-ng/helm/utils']) {
191
+ delete paths['@spartan-ng/helm/utils'];
192
+ updated = true;
193
+ }
194
+ if (paths['@spartan-ng/helm/*']) {
195
+ delete paths['@spartan-ng/helm/*'];
196
+ updated = true;
197
+ }
198
+ if (updated) {
199
+ fs.writeJsonSync(tsconfigPath, tsconfig, { spaces: 2 });
200
+ console.log('[Spartan] Aliases genéricos antigos removidos antes da geração.');
201
+ }
202
+ }
203
+ function removeSpartanGeneratedUi(frontendRoot) {
204
+ const uiRoot = path.join(frontendRoot, 'libs', 'ui');
205
+ if (fs.existsSync(uiRoot)) {
206
+ fs.removeSync(uiRoot);
207
+ console.log('[Spartan] libs/ui removido para regenerar o style real.');
208
+ }
209
+ const tsconfigPath = path.join(frontendRoot, 'tsconfig.json');
210
+ if (!fs.existsSync(tsconfigPath))
211
+ return;
212
+ const tsconfig = fs.readJsonSync(tsconfigPath);
213
+ const paths = tsconfig.compilerOptions?.paths;
214
+ if (!paths)
215
+ return;
216
+ let updated = false;
217
+ for (const alias of Object.keys(paths)) {
218
+ if (alias === '@spartan-ng/helm/utils' || alias.startsWith('@spartan-ng/helm/')) {
219
+ delete paths[alias];
220
+ updated = true;
221
+ }
222
+ }
223
+ if (updated) {
224
+ fs.writeJsonSync(tsconfigPath, tsconfig, { spaces: 2 });
225
+ console.log('[Spartan] Aliases Helm removidos para forçar nova geração.');
226
+ }
227
+ }
228
+ function ensureSpartanTailwindSource(frontendRoot) {
229
+ const styleCandidates = [
230
+ path.join(frontendRoot, 'src', 'styles.scss'),
231
+ path.join(frontendRoot, 'src', 'styles.css')
232
+ ];
233
+ const stylesPath = styleCandidates.find((candidate) => fs.existsSync(candidate));
234
+ if (!stylesPath)
235
+ return;
236
+ let content = fs.readFileSync(stylesPath, 'utf-8');
237
+ if (!content.includes("@source './app'") && !content.includes('@source "./app"')) {
238
+ content = content.replace(/(@import ['"]@angular\/cdk\/overlay-prebuilt\.css['"];?)/, `$1\n\n@source './app';`);
239
+ }
240
+ if (!content.includes("@source '../libs/ui'") && !content.includes('@source "../libs/ui"')) {
241
+ content = content.replace(/(@import ['"]@spartan-ng\/brain\/hlm-tailwind-preset\.css['"];?)/, `$1\n\n@source '../libs/ui';`);
242
+ }
243
+ fs.writeFileSync(stylesPath, content, 'utf-8');
244
+ console.log('[Spartan] @source ./app e ../libs/ui garantidos nos estilos globais.');
245
+ }
246
+ function ensureSpartanProvider(frontendRoot) {
247
+ const appConfigPath = path.join(frontendRoot, 'src', 'app', 'app.config.ts');
248
+ if (!fs.existsSync(appConfigPath))
249
+ return;
250
+ let content = fs.readFileSync(appConfigPath, 'utf-8');
251
+ if (!content.includes("provideSpartanHlm")) {
252
+ content = content.replace("import { ApplicationConfig } from '@angular/core';", "import { ApplicationConfig } from '@angular/core';\nimport { provideSpartanHlm } from '@spartan-ng/helm/utils';");
253
+ content = content.replace(/providers:\s*\[/, 'providers: [\n provideSpartanHlm(),');
254
+ fs.writeFileSync(appConfigPath, content, 'utf-8');
255
+ console.log('[Spartan] provideSpartanHlm() registrado no app.config.ts.');
256
+ }
257
+ }
258
+ function getSupportedSpartanPrimitiveNames(frontendRoot) {
259
+ const supportedLibrariesPath = path.join(frontendRoot, 'node_modules', '@spartan-ng', 'cli', 'src', 'generators', 'ui', 'supported-ui-libraries.json');
260
+ if (!fs.existsSync(supportedLibrariesPath)) {
261
+ throw new Error('[Spartan] @spartan-ng/cli não encontrado em node_modules. Instale as dependências do frontend antes de executar init-ui.');
262
+ }
263
+ const supportedLibraries = JSON.parse(fs.readFileSync(supportedLibrariesPath, 'utf-8'));
264
+ return Object.keys(supportedLibraries).sort();
265
+ }
266
+ function runSpartanGenerator(frontendRoot, style, forceRegenerate = false) {
267
+ ensureSpartanComponentsJson(frontendRoot, style);
268
+ if (forceRegenerate) {
269
+ removeSpartanGeneratedUi(frontendRoot);
270
+ }
271
+ removeStaleSpartanTsconfigPaths(frontendRoot);
272
+ ensureSpartanTailwindSource(frontendRoot);
273
+ ensureSpartanProvider(frontendRoot);
274
+ const ng = getPackageManagerCommand(frontendRoot);
275
+ const primitiveNames = getSupportedSpartanPrimitiveNames(frontendRoot);
276
+ console.log(`[Spartan] Gerando ${primitiveNames.length} primitives Helm reais (${style})...`);
277
+ for (const primitiveName of primitiveNames) {
278
+ (0, child_process_1.execSync)(`${ng} g @spartan-ng/cli:ui ${primitiveName} --defaults --force`, {
279
+ cwd: frontendRoot,
280
+ stdio: 'inherit'
281
+ });
282
+ }
283
+ ensureSpartanTsconfigPaths(frontendRoot);
284
+ }
285
+ function ensureOfficialSpartanTheme(frontendRoot, theme = 'neutral') {
286
+ const stylesPath = [
287
+ path.join(frontendRoot, 'src', 'styles.css'),
288
+ path.join(frontendRoot, 'src', 'styles.scss')
289
+ ].find((candidate) => fs.existsSync(candidate));
290
+ if (!stylesPath)
291
+ return;
292
+ const currentStyles = fs.readFileSync(stylesPath, 'utf-8');
293
+ if (/--radius\s*:/.test(currentStyles) && /--sidebar-background|--sidebar\s*:/.test(currentStyles)) {
294
+ return;
295
+ }
296
+ const ng = getPackageManagerCommand(frontendRoot);
297
+ console.log(`[Spartan] Aplicando tema oficial "${theme}"...`);
298
+ (0, child_process_1.execSync)(`${ng} g @spartan-ng/cli:ui-theme --theme=${theme} --defaults`, {
299
+ cwd: frontendRoot,
300
+ stdio: 'inherit'
301
+ });
302
+ }
303
+ function runSpartanPrimitiveGenerator(frontendRoot, style, primitiveName) {
304
+ ensureSpartanComponentsJson(frontendRoot, style);
305
+ removeStaleSpartanTsconfigPaths(frontendRoot);
306
+ ensureSpartanTailwindSource(frontendRoot);
307
+ ensureSpartanProvider(frontendRoot);
308
+ const supportedPrimitives = getSupportedSpartanPrimitiveNames(frontendRoot);
309
+ if (!supportedPrimitives.includes(primitiveName)) {
310
+ throw new Error(`[Spartan] Primitive "${primitiveName}" não existe na lista oficial do Spartan CLI.`);
311
+ }
312
+ const ng = getPackageManagerCommand(frontendRoot);
313
+ console.log(`[Spartan] Gerando primitive Helm real "${primitiveName}" (${style})...`);
314
+ (0, child_process_1.execSync)(`${ng} g @spartan-ng/cli:ui ${primitiveName} --defaults --force`, {
315
+ cwd: frontendRoot,
316
+ stdio: 'inherit'
317
+ });
318
+ ensureSpartanTsconfigPaths(frontendRoot);
319
+ }
320
+ function createBoilerplateCopyFilter(templateRoot) {
321
+ const ignored = new Set([
322
+ 'node_modules',
323
+ '.angular',
324
+ 'dist',
325
+ 'coverage',
326
+ '.turbo',
327
+ '.nx'
328
+ ]);
329
+ return (src) => {
330
+ const relativePath = path.relative(templateRoot, src);
331
+ return !relativePath.split(path.sep).some((part) => ignored.has(part));
332
+ };
333
+ }
334
+ // Helper para garantir e configurar os caminhos de path alias no tsconfig.json do Frontend
335
+ function ensureTsconfigPaths(frontendRoot) {
336
+ const tsconfigPath = path.join(frontendRoot, 'tsconfig.json');
337
+ if (!fs.existsSync(tsconfigPath)) {
338
+ console.warn(`[Aviso] tsconfig.json não encontrado em ${frontendRoot}. Certifique-se de configurar os path aliases manualmente.`);
339
+ return;
340
+ }
341
+ try {
342
+ const tsconfig = fs.readJsonSync(tsconfigPath);
343
+ if (!tsconfig.compilerOptions) {
344
+ tsconfig.compilerOptions = {};
345
+ }
346
+ if (!tsconfig.compilerOptions.paths) {
347
+ tsconfig.compilerOptions.paths = {};
348
+ }
349
+ const aliasKey = '@devstroupe/ui/*';
350
+ const aliasVal = ['src/app/shared/components/devkit/*'];
351
+ const spartanUtilsKey = '@spartan-ng/helm/utils';
352
+ const spartanUtilsVal = ['libs/ui/utils/src/index.ts'];
353
+ const spartanAliasKey = '@spartan-ng/helm/*';
354
+ const spartanAliasVal = ['libs/ui/*/src/index.ts'];
355
+ let updated = false;
356
+ if (!tsconfig.compilerOptions.paths[aliasKey] ||
357
+ JSON.stringify(tsconfig.compilerOptions.paths[aliasKey]) !== JSON.stringify(aliasVal)) {
358
+ tsconfig.compilerOptions.paths[aliasKey] = aliasVal;
359
+ updated = true;
360
+ }
361
+ if (!tsconfig.compilerOptions.paths[spartanUtilsKey] ||
362
+ JSON.stringify(tsconfig.compilerOptions.paths[spartanUtilsKey]) !== JSON.stringify(spartanUtilsVal)) {
363
+ tsconfig.compilerOptions.paths[spartanUtilsKey] = spartanUtilsVal;
364
+ updated = true;
365
+ }
366
+ if (!tsconfig.compilerOptions.paths[spartanAliasKey] ||
367
+ JSON.stringify(tsconfig.compilerOptions.paths[spartanAliasKey]) !== JSON.stringify(spartanAliasVal)) {
368
+ tsconfig.compilerOptions.paths[spartanAliasKey] = spartanAliasVal;
369
+ updated = true;
370
+ }
371
+ if (updated) {
372
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2), 'utf-8');
373
+ console.log(`[TSConfig] Path aliases configurados com sucesso em: ${tsconfigPath}`);
374
+ }
375
+ }
376
+ catch (err) {
377
+ console.error(`[Erro] Falha ao atualizar o tsconfig.json em ${tsconfigPath}:`, err);
378
+ }
379
+ }
380
+ function ensureIconDependencies(frontendRoot) {
381
+ const pkgJsonPath = path.join(frontendRoot, 'package.json');
382
+ if (!fs.existsSync(pkgJsonPath))
383
+ return;
384
+ try {
385
+ const pkgJson = fs.readJsonSync(pkgJsonPath);
386
+ const deps = ['@ng-icons/core', '@ng-icons/lucide'];
387
+ const missingDeps = deps.filter(dep => {
388
+ const hasDep = (pkgJson.dependencies && pkgJson.dependencies[dep]) ||
389
+ (pkgJson.devDependencies && pkgJson.devDependencies[dep]);
390
+ return !hasDep;
391
+ });
392
+ if (missingDeps.length > 0) {
393
+ console.log(`[Icones] Instalando dependências de ícones necessárias: ${missingDeps.join(', ')}...`);
394
+ const workspaceRoot = process.cwd();
395
+ const isPnpm = fs.existsSync(path.join(workspaceRoot, 'pnpm-lock.yaml'));
396
+ const { execSync } = require('child_process');
397
+ for (const dep of missingDeps) {
398
+ if (isPnpm) {
399
+ execSync(`pnpm --dir "${frontendRoot}" add ${dep}`, { stdio: 'inherit' });
400
+ }
401
+ else {
402
+ execSync(`npm --prefix "${frontendRoot}" install ${dep} --legacy-peer-deps`, { stdio: 'inherit' });
403
+ }
404
+ }
405
+ console.log('[Icones] Dependências de ícones instaladas com sucesso!');
406
+ }
407
+ }
408
+ catch (err) {
409
+ console.warn('[Aviso] Falha ao instalar automaticamente dependências de ícones:', err);
410
+ }
411
+ }
412
+ // Helper para inicializar o Design System local copiando os arquivos de template de UI
413
+ function initUi(frontendRoot, config) {
414
+ ensureIconDependencies(frontendRoot);
415
+ const designSystemConfig = config.designSystem || {};
416
+ const projectName = config.projectName || 'DevsTroupe Project';
417
+ // Obter navegação configurada ou padrão
418
+ let navigation = [];
419
+ if (config.layout && config.layout.navigation) {
420
+ navigation = config.layout.navigation;
421
+ }
422
+ else {
423
+ navigation = [
424
+ { label: 'Dashboard', icon: 'lucideHome', route: '/dashboard' },
425
+ { label: 'Playground', icon: 'lucidePalette', route: '/playground' }
426
+ ];
427
+ if (config.entities && Array.isArray(config.entities)) {
428
+ config.entities.forEach((entity) => {
429
+ navigation.push({
430
+ label: entity.label || entity.name,
431
+ icon: 'lucideTable',
432
+ route: `/${entity.name}`
433
+ });
434
+ });
435
+ }
436
+ }
437
+ const targetDir = path.join(frontendRoot, 'src', 'app', 'shared', 'components', 'devkit');
438
+ const layoutsDir = path.join(frontendRoot, 'src', 'app', 'shared', 'layouts', 'main-layout');
439
+ const spartanStyle = getSpartanStyle(config);
440
+ console.log(`Injetando componentes locais do Design System em: ${targetDir}...`);
441
+ runSpartanGenerator(frontendRoot, spartanStyle);
442
+ ensureOfficialSpartanTheme(frontendRoot, designSystemConfig.spartanTheme || 'neutral');
443
+ // Garantir diretórios dos componentes
444
+ fs.ensureDirSync(targetDir);
445
+ fs.ensureDirSync(path.join(targetDir, 'button'));
446
+ fs.ensureDirSync(path.join(targetDir, 'input'));
447
+ fs.ensureDirSync(path.join(targetDir, 'select'));
448
+ fs.ensureDirSync(path.join(targetDir, 'table'));
449
+ fs.ensureDirSync(path.join(targetDir, 'filter'));
450
+ fs.ensureDirSync(path.join(targetDir, 'card-list'));
451
+ fs.ensureDirSync(path.join(targetDir, 'simple-list'));
452
+ fs.ensureDirSync(path.join(targetDir, 'dialog'));
453
+ fs.ensureDirSync(path.join(targetDir, 'shell'));
454
+ fs.ensureDirSync(path.join(targetDir, 'sidebar'));
455
+ fs.ensureDirSync(path.join(targetDir, 'header'));
456
+ fs.ensureDirSync(path.join(targetDir, 'ui-badge-helm'));
457
+ fs.ensureDirSync(path.join(targetDir, 'ui-avatar-helm'));
458
+ // Garantir diretório do Layout
459
+ fs.ensureDirSync(layoutsDir);
460
+ // O tema e os tokens visuais pertencem ao gerador oficial do Spartan.
461
+ fs.writeFileSync(path.join(targetDir, 'README.md'), (0, ui_templates_1.devkitLocalReadmeTemplate)(), 'utf-8');
462
+ // 2. Escrever Button Component
463
+ const btn = (0, ui_templates_1.devkitButtonTemplate)();
464
+ fs.writeFileSync(path.join(targetDir, 'button', 'button.component.ts'), btn.ts, 'utf-8');
465
+ fs.writeFileSync(path.join(targetDir, 'button', 'button.component.html'), btn.html, 'utf-8');
466
+ fs.writeFileSync(path.join(targetDir, 'button', 'button.component.css'), btn.css, 'utf-8');
467
+ // 3. Escrever Input Component
468
+ const inp = (0, ui_templates_1.devkitInputTemplate)();
469
+ fs.writeFileSync(path.join(targetDir, 'input', 'input.component.ts'), inp.ts, 'utf-8');
470
+ fs.writeFileSync(path.join(targetDir, 'input', 'input.component.html'), inp.html, 'utf-8');
471
+ // 4. Escrever Select Component
472
+ const sel = (0, ui_templates_1.devkitSelectTemplate)();
473
+ fs.writeFileSync(path.join(targetDir, 'select', 'select.component.ts'), sel.ts, 'utf-8');
474
+ fs.writeFileSync(path.join(targetDir, 'select', 'select.component.html'), sel.html, 'utf-8');
475
+ // 5. Escrever Table Component
476
+ const tbl = (0, ui_templates_1.devkitTableTemplate)();
477
+ fs.writeFileSync(path.join(targetDir, 'table', 'table.component.ts'), tbl.ts, 'utf-8');
478
+ fs.writeFileSync(path.join(targetDir, 'table', 'table.component.html'), tbl.html, 'utf-8');
479
+ fs.writeFileSync(path.join(targetDir, 'table', 'table.component.css'), tbl.css, 'utf-8');
480
+ // 6. Escrever Filter Component
481
+ const flt = (0, ui_templates_1.devkitFilterTemplate)();
482
+ fs.writeFileSync(path.join(targetDir, 'filter', 'filter.component.ts'), flt.ts, 'utf-8');
483
+ fs.writeFileSync(path.join(targetDir, 'filter', 'filter.component.html'), flt.html, 'utf-8');
484
+ // 7. Escrever Card List Component
485
+ const crd = (0, ui_templates_1.devkitCardListTemplate)();
486
+ fs.writeFileSync(path.join(targetDir, 'card-list', 'card-list.component.ts'), crd.ts, 'utf-8');
487
+ fs.writeFileSync(path.join(targetDir, 'card-list', 'card-list.component.html'), crd.html, 'utf-8');
488
+ fs.writeFileSync(path.join(targetDir, 'card-list', 'card-list.component.css'), crd.css, 'utf-8');
489
+ // 8. Escrever Simple List Component
490
+ const smp = (0, ui_templates_1.devkitSimpleListTemplate)();
491
+ fs.writeFileSync(path.join(targetDir, 'simple-list', 'simple-list.component.ts'), smp.ts, 'utf-8');
492
+ fs.writeFileSync(path.join(targetDir, 'simple-list', 'simple-list.component.html'), smp.html, 'utf-8');
493
+ fs.writeFileSync(path.join(targetDir, 'simple-list', 'simple-list.component.css'), smp.css, 'utf-8');
494
+ // 9. Escrever Dialog Component
495
+ const dlg = (0, ui_templates_1.devkitDialogTemplate)();
496
+ fs.writeFileSync(path.join(targetDir, 'dialog', 'dialog.component.ts'), dlg.ts, 'utf-8');
497
+ fs.writeFileSync(path.join(targetDir, 'dialog', 'dialog.component.html'), dlg.html, 'utf-8');
498
+ fs.writeFileSync(path.join(targetDir, 'dialog', 'dialog.component.css'), dlg.css, 'utf-8');
499
+ // 10. Escrever Shell Component
500
+ const shl = (0, ui_templates_1.devkitShellTemplate)();
501
+ fs.writeFileSync(path.join(targetDir, 'shell', 'shell.component.ts'), shl.ts, 'utf-8');
502
+ fs.writeFileSync(path.join(targetDir, 'shell', 'shell.component.html'), shl.html, 'utf-8');
503
+ fs.writeFileSync(path.join(targetDir, 'shell', 'shell.component.css'), shl.css, 'utf-8');
504
+ // 11. Escrever Sidebar Component
505
+ const sdb = (0, ui_templates_1.devkitSidebarTemplate)();
506
+ fs.writeFileSync(path.join(targetDir, 'sidebar', 'sidebar.component.ts'), sdb.ts, 'utf-8');
507
+ fs.writeFileSync(path.join(targetDir, 'sidebar', 'sidebar.component.html'), sdb.html, 'utf-8');
508
+ fs.writeFileSync(path.join(targetDir, 'sidebar', 'sidebar.component.css'), sdb.css, 'utf-8');
509
+ // 12. Escrever Header Component
510
+ const hdr = (0, ui_templates_1.devkitHeaderTemplate)();
511
+ fs.writeFileSync(path.join(targetDir, 'header', 'header.component.ts'), hdr.ts, 'utf-8');
512
+ fs.writeFileSync(path.join(targetDir, 'header', 'header.component.html'), hdr.html, 'utf-8');
513
+ fs.writeFileSync(path.join(targetDir, 'header', 'header.component.css'), hdr.css, 'utf-8');
514
+ // 13. Escrever MainLayout Component
515
+ writeAppNavigation(frontendRoot, navigation);
516
+ const brandLogo = config.layout?.brandLogo || '';
517
+ const brandLogoCollapsed = config.layout?.brandLogoCollapsed || '';
518
+ const lyt = (0, ui_templates_1.devkitMainLayoutTemplate)(projectName, brandLogo, brandLogoCollapsed);
519
+ fs.writeFileSync(path.join(layoutsDir, 'main-layout.component.ts'), lyt.ts, 'utf-8');
520
+ fs.writeFileSync(path.join(layoutsDir, 'main-layout.component.html'), lyt.html, 'utf-8');
521
+ fs.writeFileSync(path.join(layoutsDir, 'main-layout.component.css'), lyt.css, 'utf-8');
522
+ // 14. Escrever DevkitBadge Component baseado no HlmBadge real gerado pelo Spartan
523
+ const bdg = (0, ui_templates_1.devkitBadgeTemplate)();
524
+ fs.writeFileSync(path.join(targetDir, 'ui-badge-helm', 'badge.component.ts'), bdg.ts, 'utf-8');
525
+ // 15. Escrever DevkitAvatar Component
526
+ const avt = (0, ui_templates_1.devkitAvatarTemplate)();
527
+ fs.writeFileSync(path.join(targetDir, 'ui-avatar-helm', 'avatar.component.ts'), avt.ts, 'utf-8');
528
+ // 16. Escrever Playground Component
529
+ const playgroundDir = path.join(frontendRoot, 'src', 'app', 'modules', 'playground');
530
+ fs.ensureDirSync(playgroundDir);
531
+ const ply = (0, ui_templates_1.devkitPlaygroundComponentTemplate)(getSupportedSpartanPrimitiveNames(frontendRoot));
532
+ fs.writeFileSync(path.join(playgroundDir, 'playground.component.ts'), ply.ts, 'utf-8');
533
+ fs.writeFileSync(path.join(playgroundDir, 'playground.component.html'), ply.html, 'utf-8');
534
+ fs.writeFileSync(path.join(playgroundDir, 'playground.component.css'), ply.css, 'utf-8');
535
+ // 17. Escrever ThemeService
536
+ const servicesDir = path.join(frontendRoot, 'src', 'app', 'core', 'services');
537
+ fs.ensureDirSync(servicesDir);
538
+ fs.writeFileSync(path.join(servicesDir, 'theme.service.ts'), (0, ui_templates_1.devkitThemeServiceTemplate)(), 'utf-8');
539
+ // 18. Escrever ProfileService
540
+ fs.writeFileSync(path.join(servicesDir, 'profile.service.ts'), (0, ui_templates_1.devkitProfileServiceTemplate)(), 'utf-8');
541
+ // 19. Escrever ProfileComponent
542
+ const profileDir = path.join(frontendRoot, 'src', 'app', 'modules', 'profile');
543
+ fs.ensureDirSync(profileDir);
544
+ const prf = (0, ui_templates_1.devkitProfileComponentTemplate)();
545
+ fs.writeFileSync(path.join(profileDir, 'profile.component.ts'), prf.ts, 'utf-8');
546
+ fs.writeFileSync(path.join(profileDir, 'profile.component.html'), prf.html, 'utf-8');
547
+ fs.writeFileSync(path.join(profileDir, 'profile.component.css'), prf.css, 'utf-8');
548
+ console.log(`[UI] Componentes DevKit, layout, Playground, ThemeService, ProfileService e ProfileComponent gerados com Spartan style="${spartanStyle}".`);
549
+ // Configurar tsconfig.json
550
+ ensureTsconfigPaths(frontendRoot);
551
+ // Atualizar roteamento para suportar MainLayoutComponent
552
+ updateAppRoutes(frontendRoot, config);
553
+ }
554
+ function updateAppRoutes(frontendRoot, config = {}) {
555
+ const routesPath = path.join(frontendRoot, 'src', 'app', 'app.routes.ts');
556
+ if (!fs.existsSync(routesPath)) {
557
+ return;
558
+ }
559
+ // Filtrar entidades que possuem pasta em src/app/modules
560
+ const entities = Array.isArray(config) ? config : config.entities;
561
+ const activeEntities = (entities || []).filter((entity) => {
562
+ if (!entity || !entity.name)
563
+ return false;
564
+ const modulePath = path.join(frontendRoot, 'src', 'app', 'modules', entity.name);
565
+ return fs.existsSync(modulePath);
566
+ });
567
+ let routeImports = '';
568
+ let entityRoutes = '';
569
+ activeEntities.forEach((entity) => {
570
+ const pascalName = kebabToPascal(entity.name);
571
+ const formDialogPath = path.join(frontendRoot, 'src', 'app', 'modules', entity.name, 'form-dialog-handler');
572
+ const hasDialog = fs.existsSync(formDialogPath);
573
+ const routeConstant = `${entity.name.replace(/-/g, '_').toUpperCase()}_ROUTES`;
574
+ const featureRoutesPath = path.join(frontendRoot, 'src', 'app', 'modules', entity.name, `${entity.name}.routes.ts`);
575
+ let featureRoutes = `import { Routes } from '@angular/router';\n\nexport const ${routeConstant}: Routes = [\n`;
576
+ if (hasDialog) {
577
+ featureRoutes += ` {\n`;
578
+ featureRoutes += ` path: '',\n`;
579
+ featureRoutes += ` loadComponent: () => import('./list/${entity.name}-list.component').then(m => m.${pascalName}ListComponent),\n`;
580
+ featureRoutes += ` children: [\n`;
581
+ featureRoutes += ` { path: 'new', loadComponent: () => import('./form-dialog-handler/${entity.name}-form-dialog-handler.component').then(m => m.${pascalName}FormDialogHandlerComponent) },\n`;
582
+ featureRoutes += ` { path: ':id/edit', loadComponent: () => import('./form-dialog-handler/${entity.name}-form-dialog-handler.component').then(m => m.${pascalName}FormDialogHandlerComponent) }\n`;
583
+ featureRoutes += ` ]\n`;
584
+ featureRoutes += ` }\n`;
585
+ }
586
+ else {
587
+ featureRoutes += ` { path: 'new', loadComponent: () => import('./form/${entity.name}-form.component').then(m => m.${pascalName}FormComponent) },\n`;
588
+ featureRoutes += ` { path: ':id/edit', loadComponent: () => import('./form/${entity.name}-form.component').then(m => m.${pascalName}FormComponent) },\n`;
589
+ featureRoutes += ` { path: '', loadComponent: () => import('./list/${entity.name}-list.component').then(m => m.${pascalName}ListComponent) }\n`;
590
+ }
591
+ featureRoutes += `];\n`;
592
+ fs.writeFileSync(featureRoutesPath, featureRoutes, 'utf-8');
593
+ routeImports += `import { ${routeConstant} } from './modules/${entity.name}/${entity.name}.routes';\n`;
594
+ entityRoutes += ` { path: '${entity.name}', children: ${routeConstant} },\n`;
595
+ });
596
+ const entityRoutesPath = path.join(frontendRoot, 'src', 'app', 'app.entity-routes.ts');
597
+ fs.writeFileSync(entityRoutesPath, `import { Routes } from '@angular/router';
598
+
599
+ ${routeImports}
600
+ // Gerado a partir do devstroupe.config.ts. As rotas de cada feature ficam no próprio módulo.
601
+ export const ENTITY_ROUTES: Routes = [
602
+ ${entityRoutes}];
603
+ `, 'utf-8');
604
+ const currentRoutes = fs.readFileSync(routesPath, 'utf-8');
605
+ if (!currentRoutes.includes('ENTITY_ROUTES')) {
606
+ const updatedRoutesContent = `import { Routes } from '@angular/router';
607
+ import { authGuard } from './core/guards/auth.guard';
608
+ import { LoginComponent } from './modules/auth/login/login.component';
609
+ import { RegisterComponent } from './modules/auth/register/register.component';
610
+ import { DashboardComponent } from './modules/dashboard/dashboard.component';
611
+ import { MainLayoutComponent } from './shared/layouts/main-layout/main-layout.component';
612
+ import { ENTITY_ROUTES } from './app.entity-routes';
613
+
614
+ export const routes: Routes = [
615
+ { path: 'login', component: LoginComponent },
616
+ { path: 'register', component: RegisterComponent },
617
+ {
618
+ path: '',
619
+ component: MainLayoutComponent,
620
+ canActivate: [authGuard],
621
+ children: [
622
+ { path: '', component: DashboardComponent },
623
+ { path: 'dashboard', component: DashboardComponent },
624
+ { path: 'profile', loadComponent: () => import('./modules/profile/profile.component').then(m => m.ProfileComponent) },
625
+ { path: 'playground', loadComponent: () => import('./modules/playground/playground.component').then(m => m.PlaygroundComponent) },
626
+ ...ENTITY_ROUTES
627
+ ]
628
+ },
629
+ { path: '**', redirectTo: '' }
630
+ ];
631
+ `;
632
+ fs.writeFileSync(routesPath, updatedRoutesContent, 'utf-8');
633
+ }
634
+ console.log('[Roteamento] app.entity-routes.ts sincronizado com as entidades ativas.');
635
+ updateMainLayoutNavigation(frontendRoot, activeEntities, config.layout?.navigation);
636
+ }
637
+ function updateMainLayoutNavigation(frontendRoot, entities = [], configuredNavigation) {
638
+ const layoutPath = path.join(frontendRoot, 'src', 'app', 'shared', 'layouts', 'main-layout', 'main-layout.component.ts');
639
+ if (!fs.existsSync(layoutPath))
640
+ return;
641
+ const navigation = configuredNavigation ?? [
642
+ { label: 'Dashboard', icon: 'lucideHome', route: '/dashboard' },
643
+ { label: 'Playground', icon: 'lucidePalette', route: '/playground' },
644
+ ...entities
645
+ .filter((entity) => !entity.navigation?.hidden)
646
+ .sort((left, right) => (left.navigation?.order ?? Number.MAX_SAFE_INTEGER) -
647
+ (right.navigation?.order ?? Number.MAX_SAFE_INTEGER))
648
+ .map((entity) => ({
649
+ label: entity.navigation?.label || entity.label || entity.name,
650
+ icon: entity.navigation?.icon || 'lucideTable',
651
+ route: `/${entity.name}`
652
+ }))
653
+ ];
654
+ writeAppNavigation(frontendRoot, navigation);
655
+ const content = fs.readFileSync(layoutPath, 'utf-8');
656
+ const declaration = 'menuItems: INavigationItem[] =';
657
+ const declarationIndex = content.indexOf(declaration);
658
+ if (declarationIndex < 0)
659
+ return;
660
+ const initializerEnd = content.indexOf(';', declarationIndex + declaration.length);
661
+ const initializer = content.slice(declarationIndex + declaration.length, initializerEnd).trim();
662
+ if (initializer === 'APP_NAVIGATION') {
663
+ console.log('[Navegação] Menu principal sincronizado com as entidades ativas.');
664
+ return;
665
+ }
666
+ const arrayStart = content.indexOf('[', declarationIndex + declaration.length);
667
+ if (arrayStart < 0)
668
+ return;
669
+ let depth = 0;
670
+ let arrayEnd = -1;
671
+ for (let index = arrayStart; index < content.length; index++) {
672
+ if (content[index] === '[')
673
+ depth++;
674
+ if (content[index] === ']')
675
+ depth--;
676
+ if (depth === 0) {
677
+ arrayEnd = index;
678
+ break;
679
+ }
680
+ }
681
+ if (arrayEnd < 0)
682
+ return;
683
+ let updated = `${content.slice(0, arrayStart)}APP_NAVIGATION${content.slice(arrayEnd + 1)}`;
684
+ if (!updated.includes("from '../../../app.navigation'")) {
685
+ const importLine = "import { APP_NAVIGATION } from '../../../app.navigation';\n";
686
+ const lastImportEnd = updated.lastIndexOf('\n', updated.lastIndexOf('import '));
687
+ updated = `${updated.slice(0, lastImportEnd + 1)}${importLine}${updated.slice(lastImportEnd + 1)}`;
688
+ }
689
+ fs.writeFileSync(layoutPath, updated, 'utf-8');
690
+ console.log('[Navegação] Menu principal sincronizado com as entidades ativas.');
691
+ }
692
+ function writeAppNavigation(frontendRoot, navigation) {
693
+ const navigationPath = path.join(frontendRoot, 'src', 'app', 'app.navigation.ts');
694
+ const items = JSON.stringify(navigation, null, 2)
695
+ .replace(/"(label|icon|route|badge|roles|children)":/g, '$1:');
696
+ const content = `import { INavigationItem } from './shared/components/devkit/sidebar/sidebar.component';
697
+
698
+ // Gerado a partir do devstroupe.config.ts. Customize pela configuração da entidade.
699
+ export const APP_NAVIGATION: INavigationItem[] = ${items};
700
+ `;
701
+ fs.writeFileSync(navigationPath, content, 'utf-8');
702
+ ensureNavigationIcons(frontendRoot, navigation.map((item) => item.icon).filter(Boolean));
703
+ }
704
+ function ensureNavigationIcons(frontendRoot, icons) {
705
+ const configPath = path.join(frontendRoot, 'src', 'app', 'app.config.ts');
706
+ if (!fs.existsSync(configPath))
707
+ return;
708
+ const validIcons = [...new Set(icons)].filter((icon) => /^lucide[A-Z][A-Za-z0-9]*$/.test(icon));
709
+ let content = fs.readFileSync(configPath, 'utf-8');
710
+ const missingIcons = validIcons.filter((icon) => !new RegExp(`\\b${icon}\\b`).test(content));
711
+ if (missingIcons.length === 0)
712
+ return;
713
+ content = content.replace(/([\s\S]*?)(\n} from '@ng-icons\/lucide';)/, (match, imports, suffix) => `${imports.replace(/\s*$/, '')},\n ${missingIcons.join(',\n ')}${suffix}`);
714
+ content = content.replace(/(provideIcons\(\{[\s\S]*?)(\n\s*}\))/, (match, providers, suffix) => `${providers.replace(/\s*$/, '')},\n ${missingIcons.join(',\n ')}${suffix}`);
715
+ fs.writeFileSync(configPath, content, 'utf-8');
716
+ }
717
+ program
718
+ .name('devstroupe')
719
+ .description('DevsTroupe Development Kit (DT-DevKit) CLI')
720
+ .version('1.0.0');
721
+ // Comando init
722
+ program
723
+ .command('init')
724
+ .description('Inicializa o arquivo de configuração devstroupe.config.ts na raiz do projeto')
725
+ .action(() => {
726
+ const configPath = path.join(process.cwd(), 'devstroupe.config.ts');
727
+ if (fs.existsSync(configPath)) {
728
+ console.log('O arquivo devstroupe.config.ts já existe na raiz do projeto.');
729
+ return;
730
+ }
731
+ const configContent = `export default {
732
+ projectName: 'TrevoIA',
733
+ backendPath: './backend',
734
+ frontendPath: './frontend',
735
+ designSystem: {
736
+ provider: 'spartan',
737
+ spartanStyle: 'vega', // 'nova' | 'vega' | 'lyra' | 'maia' | 'mira' | 'luma'
738
+ spartanTheme: 'neutral', // 'neutral' | 'stone' | 'zinc' | 'gray' | 'slate'
739
+ installAllPrimitives: true,
740
+ includePlayground: true,
741
+ },
742
+ entities: [
743
+ {
744
+ name: 'company',
745
+ label: 'Empresa',
746
+ tableName: 'companies',
747
+ properties: [
748
+ { name: 'name', type: 'string', required: true, label: 'Nome' },
749
+ {
750
+ name: 'cabins', type: 'relationship', required: false, label: 'Cabines',
751
+ relation: {
752
+ target: 'cabin', kind: 'one-to-many', foreignKey: 'company_id',
753
+ inverseSide: 'company', control: 'table', embedded: true,
754
+ columns: ['name', 'code', 'is_active'], lookup: { pageSize: 10 }
755
+ }
756
+ }
757
+ ],
758
+ crud: { generateFrontend: true, generateBackend: true }
759
+ },
760
+ {
761
+ name: 'cabin',
762
+ label: 'Cabine',
763
+ tableName: 'cabins',
764
+ properties: [
765
+ { name: 'name', type: 'string', required: true, label: 'Nome' },
766
+ { name: 'code', type: 'string', required: false, label: 'Código' },
767
+ {
768
+ name: 'company_id', type: 'relationship', required: true, label: 'Empresa',
769
+ relation: {
770
+ target: 'company', kind: 'many-to-one', relationName: 'company',
771
+ inverseSide: 'cabins', control: 'select', displayField: 'name', onDelete: 'CASCADE',
772
+ lookup: { mode: 'remote', pageSize: 20, minSearchLength: 0 }
773
+ }
774
+ },
775
+ { name: 'is_active', type: 'boolean', required: true, label: 'Ativo', default: true }
776
+ ],
777
+ crud: {
778
+ generateFrontend: true,
779
+ generateBackend: true
780
+ }
781
+ }
782
+ ]
783
+ };
784
+ `;
785
+ fs.writeFileSync(configPath, configContent, 'utf-8');
786
+ console.log('Arquivo devstroupe.config.ts criado com sucesso!');
787
+ });
788
+ // Novo Comando init-ui para forçar geração manual do Design System
789
+ program
790
+ .command('init-ui')
791
+ .description('Inicializa o Design System local copiando estilos e componentes primitivos para o projeto Angular')
792
+ .option('-s, --style <style>', 'Style Spartan: nova, vega, lyra, maia, mira ou luma')
793
+ .action((options) => {
794
+ const configPath = path.join(process.cwd(), 'devstroupe.config.ts');
795
+ if (!fs.existsSync(configPath)) {
796
+ console.error('Arquivo devstroupe.config.ts não encontrado na raiz. Rode "devstroupe init" primeiro.');
797
+ process.exit(1);
798
+ }
799
+ let config;
800
+ try {
801
+ const importedConfig = require(configPath);
802
+ config = importedConfig.default || importedConfig;
803
+ }
804
+ catch (error) {
805
+ console.error('Erro ao carregar devstroupe.config.ts:', error);
806
+ process.exit(1);
807
+ }
808
+ try {
809
+ assertValidRelationshipConfig(config);
810
+ }
811
+ catch (error) {
812
+ console.error('Configuração de relacionamentos inválida:', error instanceof Error ? error.message : error);
813
+ process.exit(1);
814
+ }
815
+ const frontendRoot = path.resolve(process.cwd(), config.frontendPath || './frontend');
816
+ if (!fs.existsSync(frontendRoot)) {
817
+ console.error(`Diretório frontend não encontrado em: ${frontendRoot}. Certifique-se de configurar o "frontendPath" no devstroupe.config.ts corretamente.`);
818
+ process.exit(1);
819
+ }
820
+ const requestedStyle = options.style;
821
+ if (requestedStyle) {
822
+ if (!isSpartanStyle(requestedStyle)) {
823
+ console.error(`Style Spartan inválido "${requestedStyle}". Use: ${SPARTAN_STYLES.join(', ')}.`);
824
+ process.exit(1);
825
+ }
826
+ config.designSystem = { ...(config.designSystem || {}), spartanStyle: requestedStyle };
827
+ }
828
+ initUi(frontendRoot, config);
829
+ console.log('Design System local inicializado e mapeado com sucesso!');
830
+ });
831
+ program
832
+ .command('ui:set-style <style>')
833
+ .description('Altera o style do Spartan e regenera todos os primitives Helm reais em libs/ui')
834
+ .action((style) => {
835
+ if (!isSpartanStyle(style)) {
836
+ console.error(`Style Spartan inválido "${style}". Use: ${SPARTAN_STYLES.join(', ')}.`);
837
+ process.exit(1);
838
+ }
839
+ const configPath = path.join(process.cwd(), 'devstroupe.config.ts');
840
+ if (!fs.existsSync(configPath)) {
841
+ console.error('Arquivo devstroupe.config.ts não encontrado na raiz. Rode "devstroupe init" primeiro.');
842
+ process.exit(1);
843
+ }
844
+ let config;
845
+ try {
846
+ delete require.cache[require.resolve(configPath)];
847
+ const importedConfig = require(configPath);
848
+ config = importedConfig.default || importedConfig;
849
+ }
850
+ catch (error) {
851
+ console.error('Erro ao carregar devstroupe.config.ts:', error);
852
+ process.exit(1);
853
+ }
854
+ const frontendRoot = path.resolve(process.cwd(), config.frontendPath || './frontend');
855
+ if (!fs.existsSync(frontendRoot)) {
856
+ console.error(`Diretório frontend não encontrado em: ${frontendRoot}.`);
857
+ process.exit(1);
858
+ }
859
+ runSpartanGenerator(frontendRoot, style, true);
860
+ console.log(`[Spartan] Style alterado para "${style}" e primitives regenerados em libs/ui.`);
861
+ });
862
+ // Novo comando lint:rules
863
+ program
864
+ .command('lint:rules')
865
+ .description('Audita o projeto para garantir a conformidade com as regras e boas práticas de governança da DevsTroupe')
866
+ .action(() => {
867
+ const configPath = path.join(process.cwd(), 'devstroupe.config.ts');
868
+ if (!fs.existsSync(configPath)) {
869
+ console.error('Arquivo devstroupe.config.ts não encontrado na raiz. Rode "devstroupe init" primeiro.');
870
+ process.exit(1);
871
+ }
872
+ let config;
873
+ try {
874
+ const importedConfig = require(configPath);
875
+ config = importedConfig.default || importedConfig;
876
+ }
877
+ catch (error) {
878
+ console.error('Erro ao carregar devstroupe.config.ts:', error);
879
+ process.exit(1);
880
+ }
881
+ console.log('Iniciando varredura estática de governança da DevsTroupe...');
882
+ const result = (0, linter_rules_1.runLinter)(process.cwd(), config);
883
+ console.log(`\nVarredura finalizada. Escaneados ${result.filesScanned} arquivos.`);
884
+ if (result.violations.length === 0) {
885
+ console.log('\x1b[32m[SUCESSO] Nenhuma violação de governança encontrada! O projeto segue todos os padrões de código.\x1b[0m');
886
+ process.exit(0);
887
+ }
888
+ console.log(`Encontradas ${result.violations.length} violações de governança:\n`);
889
+ let errorCount = 0;
890
+ let warnCount = 0;
891
+ result.violations.forEach((v) => {
892
+ const isError = v.severity === 'error';
893
+ if (isError)
894
+ errorCount++;
895
+ else
896
+ warnCount++;
897
+ const severityTag = isError ? '\x1b[31m[ERRO]\x1b[0m' : '\x1b[33m[AVISO]\x1b[0m';
898
+ console.log(`${severityTag} ${v.file}:${v.line} (${v.rule})`);
899
+ console.log(` => ${v.message}\n`);
900
+ });
901
+ console.log(`Resumo: ${errorCount} erros críticos, ${warnCount} avisos de atenção.`);
902
+ if (errorCount > 0) {
903
+ console.error('\x1b[31m[FALHA] A validação de governança falhou devido a erros críticos.\x1b[0m');
904
+ process.exit(1);
905
+ }
906
+ else {
907
+ console.log('\x1b[33m[AVISO] Varredura passou com avisos secundários.\x1b[0m');
908
+ process.exit(0);
909
+ }
910
+ });
911
+ // Comando generate crud
912
+ program
913
+ .command('generate <type> <entityName>')
914
+ .description('Gera artefatos do DevKit. Tipos suportados: crud')
915
+ .action((type, entityName) => {
916
+ if (type !== 'crud') {
917
+ console.error(`Tipo de geração "${type}" não é suportado. Use "crud".`);
918
+ process.exit(1);
919
+ }
920
+ const configPath = path.join(process.cwd(), 'devstroupe.config.ts');
921
+ if (!fs.existsSync(configPath)) {
922
+ console.error('Arquivo devstroupe.config.ts não encontrado na raiz. Rode "devstroupe init" primeiro.');
923
+ process.exit(1);
924
+ }
925
+ let config;
926
+ try {
927
+ const importedConfig = require(configPath);
928
+ config = importedConfig.default || importedConfig;
929
+ }
930
+ catch (error) {
931
+ console.error('Erro ao carregar devstroupe.config.ts:', error);
932
+ process.exit(1);
933
+ }
934
+ try {
935
+ assertValidRelationshipConfig(config);
936
+ }
937
+ catch (error) {
938
+ console.error('Configuração de relacionamentos inválida:', error instanceof Error ? error.message : error);
939
+ process.exit(1);
940
+ }
941
+ const entity = config.entities?.find((e) => e.name.toLowerCase() === entityName.toLowerCase());
942
+ if (!entity) {
943
+ console.error(`Entidade "${entityName}" não configurada no devstroupe.config.ts.`);
944
+ process.exit(1);
945
+ }
946
+ const backendRoot = path.resolve(process.cwd(), config.backendPath || './backend');
947
+ const frontendRoot = path.resolve(process.cwd(), config.frontendPath || './frontend');
948
+ // 1. Gerar Backend (NestJS)
949
+ if (entity.crud?.generateBackend !== false) {
950
+ const isMultiTenant = !!config.multiTenant?.enabled;
951
+ const isTenantScoped = isMultiTenant && (entity.tenantScoped !== false);
952
+ const isMicroservices = config.backendType === 'microservices';
953
+ const backendProperties = isMicroservices
954
+ ? entity.properties
955
+ .filter((property) => property.type !== 'relationship' || (0, relationships_1.getRelationships)([property])[0].kind !== 'one-to-many')
956
+ .map((property) => property.type === 'relationship'
957
+ ? { ...property, type: 'number', name: (0, relationships_1.getRelationships)([property])[0].foreignKey }
958
+ : property)
959
+ : entity.properties;
960
+ let boilerplatesDir = path.resolve(__dirname, 'boilerplates');
961
+ if (!fs.existsSync(boilerplatesDir)) {
962
+ boilerplatesDir = path.resolve(__dirname, '../boilerplates');
963
+ }
964
+ if (!fs.existsSync(boilerplatesDir)) {
965
+ boilerplatesDir = path.resolve(__dirname, '../../boilerplates');
966
+ }
967
+ const nestTemplateDir = path.join(boilerplatesDir, 'nest-template');
968
+ if (isMicroservices) {
969
+ const entityIndex = config.entities.findIndex((e) => e.name.toLowerCase() === entity.name.toLowerCase());
970
+ const port = 3001 + (entityIndex >= 0 ? entityIndex : 0);
971
+ // Garantir estrutura do API Gateway
972
+ const gatewayRoot = path.join(backendRoot, 'api-gateway');
973
+ if (!fs.existsSync(gatewayRoot) || !fs.existsSync(path.join(gatewayRoot, 'src', 'app.module.ts'))) {
974
+ console.log(`[Gateway] Inicializando estrutura do API Gateway...`);
975
+ fs.ensureDirSync(gatewayRoot);
976
+ fs.copySync(nestTemplateDir, gatewayRoot);
977
+ const gatewayEnv = `PORT=13000
978
+ DB_HOST=database
979
+ DB_PORT=3306
980
+ DB_USER=root
981
+ DB_PASSWORD=root
982
+ DB_NAME=devstroupe
983
+ JWT_SECRET=devstroupe-secret-key-12345
984
+ `;
985
+ fs.writeFileSync(path.join(gatewayRoot, '.env'), gatewayEnv, 'utf-8');
986
+ fs.writeFileSync(path.join(gatewayRoot, '.env.example'), gatewayEnv, 'utf-8');
987
+ patchLocalDependencies(gatewayRoot, null, boilerplatesDir);
988
+ try {
989
+ console.log(`[Gateway] Instalando dependências em: ${gatewayRoot}...`);
990
+ (0, child_process_1.execSync)('npm install', { cwd: gatewayRoot, stdio: 'inherit' });
991
+ }
992
+ catch (err) {
993
+ console.warn(`[Aviso] Falha ao rodar "npm install" no API Gateway. Instale manualmente.`);
994
+ }
995
+ }
996
+ const msRoot = path.join(backendRoot, 'services', entity.name);
997
+ console.log(`Gerando Microsserviço para a entidade "${entityName}" em: ${msRoot}...`);
998
+ if (!fs.existsSync(msRoot) || !fs.existsSync(path.join(msRoot, 'src', 'app.module.ts'))) {
999
+ console.log(`[Microsserviço] Inicializando estrutura do microsserviço...`);
1000
+ fs.ensureDirSync(msRoot);
1001
+ fs.copySync(nestTemplateDir, msRoot);
1002
+ // Limpa módulos desnecessários
1003
+ fs.removeSync(path.join(msRoot, 'src', 'modules', 'user'));
1004
+ fs.removeSync(path.join(msRoot, 'src', 'modules', 'auth'));
1005
+ // Injeta arquivos específicos de microsserviço
1006
+ fs.writeFileSync(path.join(msRoot, 'src', 'main.ts'), generateMsMainTs(port), 'utf-8');
1007
+ fs.writeFileSync(path.join(msRoot, 'src', 'app.module.ts'), generateMsAppModule(entity.name), 'utf-8');
1008
+ // Injeta env básico no microsserviço
1009
+ const msEnv = `PORT=${port}
1010
+ DB_HOST=database
1011
+ DB_PORT=3306
1012
+ DB_USER=root
1013
+ DB_PASSWORD=root
1014
+ DB_NAME=devstroupe_${entity.name.toLowerCase()}
1015
+ SERVICE_PORT=${port}
1016
+ `;
1017
+ fs.writeFileSync(path.join(msRoot, '.env'), msEnv, 'utf-8');
1018
+ fs.writeFileSync(path.join(msRoot, '.env.example'), msEnv, 'utf-8');
1019
+ patchLocalDependencies(msRoot, null, boilerplatesDir);
1020
+ try {
1021
+ console.log(`[Microsserviço] Instalando dependências em: ${msRoot}...`);
1022
+ (0, child_process_1.execSync)('npm install', { cwd: msRoot, stdio: 'inherit' });
1023
+ }
1024
+ catch (err) {
1025
+ console.warn(`[Aviso] Falha ao rodar "npm install" no microsserviço. Instale manualmente.`);
1026
+ }
1027
+ }
1028
+ const modulePath = path.join(msRoot, 'src', 'modules', entity.name);
1029
+ fs.ensureDirSync(path.join(modulePath, 'domain'));
1030
+ fs.ensureDirSync(path.join(modulePath, 'service'));
1031
+ fs.ensureDirSync(path.join(modulePath, 'infra', 'database'));
1032
+ fs.ensureDirSync(path.join(modulePath, 'infra', 'http'));
1033
+ // Domain Entity
1034
+ fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, backendProperties, isTenantScoped), 'utf-8');
1035
+ // Repository Port
1036
+ fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.repository.port.ts`), (0, cli_templates_1.nestRepositoryPortTemplate)(entity.name), 'utf-8');
1037
+ // DTO
1038
+ fs.writeFileSync(path.join(modulePath, 'infra', 'http', `${entity.name}.dto.ts`), (0, cli_templates_1.nestDtoTemplate)(entity.name, backendProperties), 'utf-8');
1039
+ // Controller (TCP)
1040
+ fs.writeFileSync(path.join(modulePath, 'infra', 'http', `${entity.name}.ms.controller.ts`), (0, cli_templates_1.nestMsControllerTemplate)(entity.name), 'utf-8');
1041
+ // ORM Entity
1042
+ fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.orm-entity.ts`), (0, cli_templates_1.nestOrmEntityTemplate)(entity.name, backendProperties, entity.tableName || `${entity.name}s`, isTenantScoped), 'utf-8');
1043
+ // Repository Adapter
1044
+ fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.repository.ts`), (0, cli_templates_1.nestRepositoryAdapterTemplate)(entity.name, backendProperties, isTenantScoped), 'utf-8');
1045
+ // Service
1046
+ fs.writeFileSync(path.join(modulePath, 'service', `${entity.name}.service.ts`), (0, cli_templates_1.nestServiceTemplate)(entity.name), 'utf-8');
1047
+ // Module (TCP)
1048
+ fs.writeFileSync(path.join(modulePath, `${entity.name}.module.ts`), (0, cli_templates_1.nestMsModuleTemplate)(entity.name), 'utf-8');
1049
+ console.log(`[Microsserviço] Arquivos do microsserviço "${entityName}" gerados com sucesso.`);
1050
+ if (entity.crud?.generateMigration !== false) {
1051
+ ensureInitialMigrations(msRoot, [entity], new Map([[entity.name, backendProperties]]));
1052
+ }
1053
+ // --- Gerar API Gateway Module ---
1054
+ const gatewayModulePath = path.join(backendRoot, 'api-gateway', 'src', 'modules', entity.name);
1055
+ console.log(`Gerando Módulo Gateway para a entidade "${entityName}" em: ${gatewayModulePath}...`);
1056
+ fs.ensureDirSync(path.join(gatewayModulePath, 'infra', 'http'));
1057
+ // DTO (Gateway)
1058
+ fs.writeFileSync(path.join(gatewayModulePath, 'infra', 'http', `${entity.name}.dto.ts`), (0, cli_templates_1.nestDtoTemplate)(entity.name, backendProperties), 'utf-8');
1059
+ // Controller (Gateway)
1060
+ fs.writeFileSync(path.join(gatewayModulePath, 'infra', 'http', `${entity.name}.controller.ts`), (0, cli_templates_1.nestGatewayControllerTemplate)(entity.name), 'utf-8');
1061
+ // Module (Gateway)
1062
+ fs.writeFileSync(path.join(gatewayModulePath, `${entity.name}.module.ts`), (0, cli_templates_1.nestGatewayModuleTemplate)(entity.name, port), 'utf-8');
1063
+ // Patch app.module.ts do Gateway
1064
+ patchGatewayAppModule(path.join(backendRoot, 'api-gateway', 'src', 'app.module.ts'), entity.name);
1065
+ console.log(`[Gateway] Módulo Gateway "${entityName}" gerado com sucesso.`);
1066
+ }
1067
+ else {
1068
+ // Monolito
1069
+ console.log(`Gerando arquivos NestJS Monolito para a entidade "${entityName}"...`);
1070
+ const modulePath = path.join(backendRoot, 'src', 'modules', entity.name);
1071
+ fs.ensureDirSync(path.join(modulePath, 'domain'));
1072
+ fs.ensureDirSync(path.join(modulePath, 'service'));
1073
+ fs.ensureDirSync(path.join(modulePath, 'infra', 'database'));
1074
+ fs.ensureDirSync(path.join(modulePath, 'infra', 'http'));
1075
+ // Domain Entity
1076
+ fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.ts`), (0, cli_templates_1.nestDomainEntityTemplate)(entity.name, entity.properties, isTenantScoped), 'utf-8');
1077
+ // Repository Port
1078
+ fs.writeFileSync(path.join(modulePath, 'domain', `${entity.name}.repository.port.ts`), (0, cli_templates_1.nestRepositoryPortTemplate)(entity.name), 'utf-8');
1079
+ // DTO
1080
+ fs.writeFileSync(path.join(modulePath, 'infra', 'http', `${entity.name}.dto.ts`), (0, cli_templates_1.nestDtoTemplate)(entity.name, entity.properties), 'utf-8');
1081
+ // Controller
1082
+ fs.writeFileSync(path.join(modulePath, 'infra', 'http', `${entity.name}.controller.ts`), (0, cli_templates_1.nestControllerTemplate)(entity.name), 'utf-8');
1083
+ // ORM Entity
1084
+ fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.orm-entity.ts`), (0, cli_templates_1.nestOrmEntityTemplate)(entity.name, entity.properties, entity.tableName || `${entity.name}s`, isTenantScoped), 'utf-8');
1085
+ // Repository Adapter
1086
+ fs.writeFileSync(path.join(modulePath, 'infra', 'database', `${entity.name}.repository.ts`), (0, cli_templates_1.nestRepositoryAdapterTemplate)(entity.name, entity.properties, isTenantScoped), 'utf-8');
1087
+ // Service
1088
+ fs.writeFileSync(path.join(modulePath, 'service', `${entity.name}.service.ts`), (0, cli_templates_1.nestServiceTemplate)(entity.name), 'utf-8');
1089
+ // Module
1090
+ fs.writeFileSync(path.join(modulePath, `${entity.name}.module.ts`), (0, cli_templates_1.nestModuleTemplate)(entity.name), 'utf-8');
1091
+ console.log(`[NestJS] Módulo Monolito "${entityName}" gerado com sucesso em: ${modulePath}`);
1092
+ patchMonolithAppModule(path.join(backendRoot, 'src', 'app.module.ts'), entity.name);
1093
+ const migrationEntities = (config.entities ?? []).filter((configuredEntity) => configuredEntity.crud?.generateBackend !== false
1094
+ && configuredEntity.crud?.generateMigration !== false);
1095
+ if (migrationEntities.length > 0) {
1096
+ ensureInitialMigrations(backendRoot, migrationEntities);
1097
+ }
1098
+ }
1099
+ // Regerar docker-compose.yml e init.sql para sincronizar portas/bancos
1100
+ generateDockerComposeAndDbInit(process.cwd(), config);
1101
+ }
1102
+ // 2. Gerar Frontend (Angular)
1103
+ if (entity.crud?.generateFrontend !== false) {
1104
+ console.log(`Gerando arquivos Angular para a entidade "${entityName}"...`);
1105
+ const servicePath = path.join(frontendRoot, 'src', 'app', 'services');
1106
+ const modulePath = path.join(frontendRoot, 'src', 'app', 'modules', entity.name);
1107
+ const uiComponentsDir = path.join(frontendRoot, 'src', 'app', 'shared', 'components', 'devkit');
1108
+ // Hook automático: se a pasta de design system local não existir no frontend, inicializa-a primeiro!
1109
+ if (!fs.existsSync(uiComponentsDir)) {
1110
+ console.log('[Autoconfig] Design System local não encontrado. Inicializando automaticamente...');
1111
+ initUi(frontendRoot, config);
1112
+ }
1113
+ const listType = entity.crud?.listType || config.designSystem?.defaultListType || 'table';
1114
+ const formType = entity.crud?.formType || 'page';
1115
+ const filterType = entity.crud?.filterType || 'popover';
1116
+ fs.ensureDirSync(servicePath);
1117
+ fs.ensureDirSync(path.join(modulePath, 'list'));
1118
+ fs.ensureDirSync(path.join(modulePath, 'form'));
1119
+ if (formType === 'dialog') {
1120
+ fs.ensureDirSync(path.join(modulePath, 'form-dialog-handler'));
1121
+ }
1122
+ // Angular Service
1123
+ fs.writeFileSync(path.join(servicePath, `${entity.name}.service.ts`), (0, cli_templates_1.angularServiceTemplate)(entity.name), 'utf-8');
1124
+ // List HTML
1125
+ fs.writeFileSync(path.join(modulePath, 'list', `${entity.name}-list.component.html`), (0, cli_templates_1.angularListHtmlTemplate)(entity.name, entity.properties, listType, formType, filterType), 'utf-8');
1126
+ // List TS
1127
+ fs.writeFileSync(path.join(modulePath, 'list', `${entity.name}-list.component.ts`), (0, cli_templates_1.angularListTsTemplate)(entity.name, entity.properties, listType, formType), 'utf-8');
1128
+ // Form HTML
1129
+ fs.writeFileSync(path.join(modulePath, 'form', `${entity.name}-form.component.html`), (0, cli_templates_1.angularFormHtmlTemplate)(entity.name, entity.properties, config.entities), 'utf-8');
1130
+ // Form TS
1131
+ fs.writeFileSync(path.join(modulePath, 'form', `${entity.name}-form.component.ts`), (0, cli_templates_1.angularFormTsTemplate)(entity.name, entity.properties, config.entities), 'utf-8');
1132
+ if (formType === 'dialog') {
1133
+ fs.writeFileSync(path.join(modulePath, 'form-dialog-handler', `${entity.name}-form-dialog-handler.component.html`), (0, cli_templates_1.angularFormDialogHandlerHtmlTemplate)(entity.name), 'utf-8');
1134
+ fs.writeFileSync(path.join(modulePath, 'form-dialog-handler', `${entity.name}-form-dialog-handler.component.ts`), (0, cli_templates_1.angularFormDialogHandlerTsTemplate)(entity.name), 'utf-8');
1135
+ }
1136
+ console.log(`[Angular] Módulo "${entityName}" gerado com sucesso em: ${modulePath}`);
1137
+ console.log(`\n\x1b[32m[SUCESSO] Scaffolding de CRUD completo para "${entityName}" finalizado!\x1b[0m`);
1138
+ console.log('[Angular] Rotas e navegação serão sincronizadas automaticamente.');
1139
+ updateAppRoutes(frontendRoot, config);
1140
+ }
1141
+ });
1142
+ // Helper para linkar dependências locais se estiver rodando no monorepo de desenvolvimento
1143
+ function patchLocalDependencies(backendDest, frontendDest, boilerplatesDir) {
1144
+ const packagesDir = path.resolve(boilerplatesDir, '../../..');
1145
+ if (!fs.existsSync(packagesDir) || !fs.existsSync(path.join(packagesDir, 'devkit-core'))) {
1146
+ return;
1147
+ }
1148
+ const coreSrc = path.join(packagesDir, 'devkit-core');
1149
+ const nestSrc = path.join(packagesDir, 'devkit-nest');
1150
+ const angularSrc = path.join(packagesDir, 'devkit-angular');
1151
+ const stripDevelopmentDependencies = (packageDir) => {
1152
+ const packagePath = path.join(packageDir, 'package.json');
1153
+ if (!fs.existsSync(packagePath))
1154
+ return;
1155
+ const packageJson = fs.readJsonSync(packagePath);
1156
+ delete packageJson.devDependencies;
1157
+ packageJson.scripts = {};
1158
+ fs.writeJsonSync(packagePath, packageJson, { spaces: 2 });
1159
+ };
1160
+ const copyFilter = (src) => {
1161
+ const relativePath = path.relative(packagesDir, src);
1162
+ const ignoreList = ['node_modules', '.git', '.angular', 'tsconfig.json', 'tsconfig.build.json', 'src', 'tests', 'test'];
1163
+ const parts = relativePath.split(path.sep);
1164
+ if (parts.length > 1) {
1165
+ const subParts = parts.slice(1);
1166
+ if (subParts.some(p => ignoreList.includes(p))) {
1167
+ return false;
1168
+ }
1169
+ const lastPart = subParts[subParts.length - 1];
1170
+ if (lastPart.endsWith('.ts') && !subParts.includes('dist')) {
1171
+ return false;
1172
+ }
1173
+ }
1174
+ return true;
1175
+ };
1176
+ // 1. Preparar e copiar para o backend
1177
+ if (backendDest && fs.existsSync(backendDest)) {
1178
+ const backendLocalPackages = path.join(backendDest, 'local_packages');
1179
+ if (fs.existsSync(coreSrc)) {
1180
+ const coreDest = path.join(backendLocalPackages, 'devkit-core');
1181
+ fs.ensureDirSync(coreDest);
1182
+ fs.copySync(coreSrc, coreDest, { filter: copyFilter });
1183
+ stripDevelopmentDependencies(coreDest);
1184
+ }
1185
+ if (fs.existsSync(nestSrc)) {
1186
+ const nestDest = path.join(backendLocalPackages, 'devkit-nest');
1187
+ fs.ensureDirSync(nestDest);
1188
+ fs.copySync(nestSrc, nestDest, { filter: copyFilter });
1189
+ stripDevelopmentDependencies(nestDest);
1190
+ // Correção de workspace:* para file:../devkit-core
1191
+ const nestPkgPath = path.join(nestDest, 'package.json');
1192
+ if (fs.existsSync(nestPkgPath)) {
1193
+ try {
1194
+ const nestPkg = fs.readJsonSync(nestPkgPath);
1195
+ if (nestPkg.dependencies && nestPkg.dependencies['@devstroupe/devkit-core'] === 'workspace:*') {
1196
+ nestPkg.dependencies['@devstroupe/devkit-core'] = 'file:../devkit-core';
1197
+ fs.writeJsonSync(nestPkgPath, nestPkg, { spaces: 2 });
1198
+ console.log('[DevConfig] Ajustado workspace:* para file:../devkit-core no devkit-nest local.');
1199
+ }
1200
+ }
1201
+ catch (e) {
1202
+ console.error('[Erro] Falha ao ajustar dependências no devkit-nest local:', e);
1203
+ }
1204
+ }
1205
+ }
1206
+ // Patch Backend package.json
1207
+ const backendPkgPath = path.join(backendDest, 'package.json');
1208
+ if (fs.existsSync(backendPkgPath)) {
1209
+ try {
1210
+ const pkg = fs.readJsonSync(backendPkgPath);
1211
+ const relCore = 'file:local_packages/devkit-core';
1212
+ const relNest = 'file:local_packages/devkit-nest';
1213
+ if (pkg.dependencies['@devstroupe/devkit-core']) {
1214
+ pkg.dependencies['@devstroupe/devkit-core'] = relCore;
1215
+ }
1216
+ if (pkg.dependencies['@devstroupe/devkit-nest']) {
1217
+ pkg.dependencies['@devstroupe/devkit-nest'] = relNest;
1218
+ }
1219
+ fs.writeJsonSync(backendPkgPath, pkg, { spaces: 2 });
1220
+ console.log('[DevConfig] Dependências locais de backend vinculadas com sucesso.');
1221
+ }
1222
+ catch (err) {
1223
+ console.error('[Erro] Falha ao injetar links locais no backend:', err);
1224
+ }
1225
+ }
1226
+ // Patch Backend Dockerfile para injetar "COPY local_packages/ ./local_packages/" antes do npm install e limpar node_modules local após instalação
1227
+ const backendDockerfile = path.join(backendDest, 'Dockerfile');
1228
+ if (fs.existsSync(backendDockerfile)) {
1229
+ try {
1230
+ let content = fs.readFileSync(backendDockerfile, 'utf-8');
1231
+ const targetStr = 'COPY package*.json ./';
1232
+ const replacementStr = 'COPY package*.json ./\nCOPY local_packages/ ./local_packages/';
1233
+ content = content.split(targetStr).join(replacementStr);
1234
+ // Injeta limpeza de node_modules locais após npm install
1235
+ content = content.replace('RUN npm install --only=production', 'RUN npm install --only=production && rm -rf local_packages/*/node_modules');
1236
+ content = content.replace('RUN npm install', 'RUN npm install && rm -rf local_packages/*/node_modules');
1237
+ fs.writeFileSync(backendDockerfile, content, 'utf-8');
1238
+ console.log('[DevConfig] Dockerfile do backend ajustado para incluir dependências locais.');
1239
+ }
1240
+ catch (err) {
1241
+ console.error('[Erro] Falha ao ajustar Dockerfile do backend:', err);
1242
+ }
1243
+ }
1244
+ // Patch Backend tsconfig.json to prevent duplicate typeorm types in local development
1245
+ const backendTsconfigPath = path.join(backendDest, 'tsconfig.json');
1246
+ if (fs.existsSync(backendTsconfigPath)) {
1247
+ try {
1248
+ const tsconfig = fs.readJsonSync(backendTsconfigPath);
1249
+ tsconfig.compilerOptions = tsconfig.compilerOptions || {};
1250
+ tsconfig.compilerOptions.paths = tsconfig.compilerOptions.paths || {};
1251
+ // Mapeia pacotes que causam problemas de duplicidade de tipo em links locais
1252
+ tsconfig.compilerOptions.paths['typeorm'] = ['node_modules/typeorm'];
1253
+ tsconfig.compilerOptions.paths['@nestjs/common'] = ['node_modules/@nestjs/common'];
1254
+ tsconfig.compilerOptions.paths['@nestjs/core'] = ['node_modules/@nestjs/core'];
1255
+ tsconfig.compilerOptions.paths['@nestjs/swagger'] = ['node_modules/@nestjs/swagger'];
1256
+ fs.writeJsonSync(backendTsconfigPath, tsconfig, { spaces: 2 });
1257
+ console.log('[DevConfig] Mapeamentos de TSConfig do backend configurados para prevenir duplicidade de pacotes.');
1258
+ }
1259
+ catch (err) {
1260
+ console.error('[Erro] Falha ao atualizar tsconfig.json do backend:', err);
1261
+ }
1262
+ }
1263
+ }
1264
+ // 2. Preparar e copiar para o frontend
1265
+ if (frontendDest && fs.existsSync(frontendDest)) {
1266
+ const frontendLocalPackages = path.join(frontendDest, 'local_packages');
1267
+ if (fs.existsSync(coreSrc)) {
1268
+ const coreDest = path.join(frontendLocalPackages, 'devkit-core');
1269
+ fs.ensureDirSync(coreDest);
1270
+ fs.copySync(coreSrc, coreDest, { filter: copyFilter });
1271
+ stripDevelopmentDependencies(coreDest);
1272
+ }
1273
+ if (fs.existsSync(angularSrc)) {
1274
+ const angularDest = path.join(frontendLocalPackages, 'devkit-angular');
1275
+ fs.ensureDirSync(angularDest);
1276
+ fs.copySync(angularSrc, angularDest, { filter: copyFilter });
1277
+ stripDevelopmentDependencies(angularDest);
1278
+ // Correção de workspace:* para file:../devkit-core
1279
+ const angularPkgPath = path.join(angularDest, 'package.json');
1280
+ if (fs.existsSync(angularPkgPath)) {
1281
+ try {
1282
+ const angularPkg = fs.readJsonSync(angularPkgPath);
1283
+ if (angularPkg.dependencies && angularPkg.dependencies['@devstroupe/devkit-core'] === 'workspace:*') {
1284
+ angularPkg.dependencies['@devstroupe/devkit-core'] = 'file:../devkit-core';
1285
+ fs.writeJsonSync(angularPkgPath, angularPkg, { spaces: 2 });
1286
+ console.log('[DevConfig] Ajustado workspace:* para file:../devkit-core no devkit-angular local.');
1287
+ }
1288
+ }
1289
+ catch (e) {
1290
+ console.error('[Erro] Falha ao ajustar dependências no devkit-angular local:', e);
1291
+ }
1292
+ }
1293
+ }
1294
+ // Patch Frontend package.json
1295
+ const frontendPkgPath = path.join(frontendDest, 'package.json');
1296
+ if (fs.existsSync(frontendPkgPath)) {
1297
+ try {
1298
+ const pkg = fs.readJsonSync(frontendPkgPath);
1299
+ const relCore = 'file:local_packages/devkit-core';
1300
+ const relAngular = 'file:local_packages/devkit-angular';
1301
+ if (pkg.dependencies['@devstroupe/devkit-core']) {
1302
+ pkg.dependencies['@devstroupe/devkit-core'] = relCore;
1303
+ }
1304
+ if (pkg.dependencies['@devstroupe/devkit-angular']) {
1305
+ pkg.dependencies['@devstroupe/devkit-angular'] = relAngular;
1306
+ }
1307
+ fs.writeJsonSync(frontendPkgPath, pkg, { spaces: 2 });
1308
+ console.log('[DevConfig] Dependências locais de frontend vinculadas com sucesso.');
1309
+ }
1310
+ catch (err) {
1311
+ console.error('[Erro] Falha ao injetar links locais no frontend:', err);
1312
+ }
1313
+ }
1314
+ // Patch Frontend Dockerfile para injetar "COPY local_packages/ ./local_packages/" antes do npm install e limpar node_modules local após instalação
1315
+ const frontendDockerfile = path.join(frontendDest, 'Dockerfile');
1316
+ if (fs.existsSync(frontendDockerfile)) {
1317
+ try {
1318
+ let content = fs.readFileSync(frontendDockerfile, 'utf-8');
1319
+ const targetStr = 'COPY package*.json ./';
1320
+ const replacementStr = 'COPY package*.json ./\nCOPY local_packages/ ./local_packages/';
1321
+ content = content.split(targetStr).join(replacementStr);
1322
+ // Injeta limpeza de node_modules locais após npm install
1323
+ content = content.replace('RUN npm install --legacy-peer-deps', 'RUN npm install --legacy-peer-deps && rm -rf local_packages/*/node_modules');
1324
+ fs.writeFileSync(frontendDockerfile, content, 'utf-8');
1325
+ console.log('[DevConfig] Dockerfile do frontend ajustado para incluir dependências locais.');
1326
+ }
1327
+ catch (err) {
1328
+ console.error('[Erro] Falha ao ajustar Dockerfile do frontend:', err);
1329
+ }
1330
+ }
1331
+ // Patch Frontend tsconfig.json to prevent duplicate angular types in local development
1332
+ const frontendTsconfigPath = path.join(frontendDest, 'tsconfig.json');
1333
+ if (fs.existsSync(frontendTsconfigPath)) {
1334
+ try {
1335
+ const tsconfig = fs.readJsonSync(frontendTsconfigPath);
1336
+ tsconfig.compilerOptions = tsconfig.compilerOptions || {};
1337
+ tsconfig.compilerOptions.paths = tsconfig.compilerOptions.paths || {};
1338
+ // Mapeia pacotes que causam problemas de duplicidade de tipo em links locais no frontend
1339
+ tsconfig.compilerOptions.paths['@angular/core'] = ['node_modules/@angular/core'];
1340
+ tsconfig.compilerOptions.paths['@angular/core/*'] = ['node_modules/@angular/core/*'];
1341
+ tsconfig.compilerOptions.paths['@angular/common'] = ['node_modules/@angular/common'];
1342
+ tsconfig.compilerOptions.paths['@angular/common/*'] = ['node_modules/@angular/common/*'];
1343
+ tsconfig.compilerOptions.paths['@angular/common/http'] = ['node_modules/@angular/common/http'];
1344
+ tsconfig.compilerOptions.paths['@angular/common/http/*'] = ['node_modules/@angular/common/http/*'];
1345
+ tsconfig.compilerOptions.paths['@angular/router'] = ['node_modules/@angular/router'];
1346
+ tsconfig.compilerOptions.paths['@angular/router/*'] = ['node_modules/@angular/router/*'];
1347
+ tsconfig.compilerOptions.paths['rxjs'] = ['node_modules/rxjs'];
1348
+ tsconfig.compilerOptions.paths['rxjs/*'] = ['node_modules/rxjs/*'];
1349
+ tsconfig.compilerOptions.paths['@devstroupe/devkit-core'] = ['node_modules/@devstroupe/devkit-core'];
1350
+ tsconfig.compilerOptions.paths['@devstroupe/devkit-core/*'] = ['node_modules/@devstroupe/devkit-core/*'];
1351
+ fs.writeJsonSync(frontendTsconfigPath, tsconfig, { spaces: 2 });
1352
+ console.log('[DevConfig] Mapeamentos de TSConfig do frontend configurados para prevenir duplicidade de pacotes.');
1353
+ }
1354
+ catch (err) {
1355
+ console.error('[Erro] Falha ao atualizar tsconfig.json do frontend:', err);
1356
+ }
1357
+ }
1358
+ }
1359
+ }
1360
+ // ---------------- Helpers para Microsserviços NestJS ----------------
1361
+ function capitalize(s) {
1362
+ return s.charAt(0).toUpperCase() + s.slice(1);
1363
+ }
1364
+ function kebabToCamel(s) {
1365
+ return s.replace(/-./g, (x) => x[1].toUpperCase());
1366
+ }
1367
+ function kebabToPascal(s) {
1368
+ return capitalize(kebabToCamel(s));
1369
+ }
1370
+ function generateMsMainTs(port) {
1371
+ return `import * as fs from 'fs';
1372
+ import * as path from 'path';
1373
+
1374
+ // Carrega variáveis do arquivo .env manualmente se ele existir
1375
+ const envPath = path.resolve(process.cwd(), '.env');
1376
+ if (fs.existsSync(envPath)) {
1377
+ const envConfig = fs.readFileSync(envPath, 'utf-8');
1378
+ envConfig.split('\\n').forEach((line) => {
1379
+ const trimmed = line.trim();
1380
+ if (trimmed && !trimmed.startsWith('#')) {
1381
+ const [key, ...values] = trimmed.split('=');
1382
+ const val = values.join('=').trim();
1383
+ if (key && val) {
1384
+ process.env[key.trim()] = val.replace(/^['"]|['"]$/g, '');
1385
+ }
1386
+ }
1387
+ });
1388
+ }
1389
+
1390
+ import { NestFactory } from '@nestjs/core';
1391
+ import { Transport, MicroserviceOptions } from '@nestjs/microservices';
1392
+ import { AppModule } from './app.module';
1393
+
1394
+ async function bootstrap() {
1395
+ const port = Number(process.env.SERVICE_PORT) || ${port};
1396
+ const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
1397
+ transport: Transport.TCP,
1398
+ options: {
1399
+ host: '0.0.0.0',
1400
+ port: port,
1401
+ },
1402
+ });
1403
+ await app.listen();
1404
+ console.log(\`Microsserviço iniciado via TCP na porta \${port}\`);
1405
+ }
1406
+ bootstrap();
1407
+ `;
1408
+ }
1409
+ function generateMsAppModule(entityName) {
1410
+ const pascalName = kebabToPascal(entityName);
1411
+ return `import { Module } from '@nestjs/common';
1412
+ import { TypeOrmModule } from '@nestjs/typeorm';
1413
+ import { ${pascalName}Module } from './modules/${entityName}/${entityName}.module';
1414
+
1415
+ @Module({
1416
+ imports: [
1417
+ TypeOrmModule.forRoot({
1418
+ type: 'mysql',
1419
+ host: process.env.DB_HOST || 'localhost',
1420
+ port: Number(process.env.DB_PORT) || 3306,
1421
+ username: process.env.DB_USER || 'root',
1422
+ password: process.env.DB_PASSWORD || 'root',
1423
+ database: process.env.DB_NAME || 'devstroupe_${entityName}',
1424
+ autoLoadEntities: true,
1425
+ synchronize: false,
1426
+ }),
1427
+ ${pascalName}Module,
1428
+ ],
1429
+ controllers: [],
1430
+ providers: [],
1431
+ })
1432
+ export class AppModule {}
1433
+ `;
1434
+ }
1435
+ function patchGatewayAppModule(appModulePath, entityName) {
1436
+ const pascalName = kebabToPascal(entityName);
1437
+ const moduleName = `${pascalName}GatewayModule`;
1438
+ const importPath = `./modules/${entityName}/${entityName}.module`;
1439
+ if (!fs.existsSync(appModulePath)) {
1440
+ console.warn(`[Aviso] Arquivo ${appModulePath} não encontrado para aplicar patch.`);
1441
+ return;
1442
+ }
1443
+ let content = fs.readFileSync(appModulePath, 'utf-8');
1444
+ // Verifica se o import já existe
1445
+ if (content.includes(moduleName)) {
1446
+ return;
1447
+ }
1448
+ // Adiciona a declaração de import no topo
1449
+ const importStatement = `import { ${moduleName} } from '${importPath}';\n`;
1450
+ content = importStatement + content;
1451
+ // Encontra a lista de imports do decorator @Module
1452
+ // Encontra a lista de imports do decorator @Module
1453
+ const moduleDecoratorRegex = /@Module\s*\(\s*\{\s*imports\s*:\s*\[([\s\S]*?)\]/;
1454
+ const match = content.match(moduleDecoratorRegex);
1455
+ if (match) {
1456
+ const currentImports = match[1];
1457
+ // Adiciona o novo módulo no início dos imports
1458
+ const updatedImports = `\n ${moduleName},${currentImports}`;
1459
+ content = content.replace(currentImports, updatedImports);
1460
+ }
1461
+ fs.writeFileSync(appModulePath, content, 'utf-8');
1462
+ console.log(`[Gateway] app.module.ts patcheado com sucesso para importar ${moduleName}.`);
1463
+ }
1464
+ function patchMonolithAppModule(appModulePath, entityName) {
1465
+ const pascalName = kebabToPascal(entityName);
1466
+ const moduleName = `${pascalName}Module`;
1467
+ const importPath = `./modules/${entityName}/${entityName}.module`;
1468
+ if (!fs.existsSync(appModulePath)) {
1469
+ console.warn(`[Aviso] Arquivo ${appModulePath} não encontrado para aplicar patch.`);
1470
+ return;
1471
+ }
1472
+ let content = fs.readFileSync(appModulePath, 'utf-8');
1473
+ if (content.includes(`import { ${moduleName} }`))
1474
+ return;
1475
+ const lastImportIndex = content.lastIndexOf('import ');
1476
+ const lastImportEnd = content.indexOf('\n', lastImportIndex);
1477
+ const importStatement = `import { ${moduleName} } from '${importPath}';`;
1478
+ content = `${content.slice(0, lastImportEnd + 1)}${importStatement}\n${content.slice(lastImportEnd + 1)}`;
1479
+ const importsArrayStart = content.indexOf('imports: [');
1480
+ if (importsArrayStart === -1) {
1481
+ console.warn(`[Aviso] Lista imports do @Module não encontrada em ${appModulePath}.`);
1482
+ return;
1483
+ }
1484
+ const insertionPoint = importsArrayStart + 'imports: ['.length;
1485
+ content = `${content.slice(0, insertionPoint)}\n ${moduleName},${content.slice(insertionPoint)}`;
1486
+ fs.writeFileSync(appModulePath, content, 'utf-8');
1487
+ console.log(`[NestJS] app.module.ts atualizado para importar ${moduleName}.`);
1488
+ }
1489
+ function generateDockerComposeAndDbInit(projectRoot, config) {
1490
+ const isMicroservices = config.backendType === 'microservices';
1491
+ const projectName = config.projectName || 'devstroupe-project';
1492
+ // 1. db-init/init.sql
1493
+ const dbInitDir = path.join(projectRoot, 'db-init');
1494
+ fs.ensureDirSync(dbInitDir);
1495
+ const dbs = ['devstroupe']; // Banco principal (usado pelo gateway ou monolith)
1496
+ if (isMicroservices && config.entities) {
1497
+ config.entities.forEach((entity) => {
1498
+ dbs.push(`devstroupe_${entity.name.toLowerCase()}`);
1499
+ });
1500
+ }
1501
+ const initSqlContent = dbs.map(db => `CREATE DATABASE IF NOT EXISTS \`${db}\`;`).join('\n') + '\n';
1502
+ fs.writeFileSync(path.join(dbInitDir, 'init.sql'), initSqlContent, 'utf-8');
1503
+ console.log(`[Docker] db-init/init.sql gerado com sucesso.`);
1504
+ // 2. docker-compose.yml
1505
+ let servicesYaml = '';
1506
+ // Database service
1507
+ servicesYaml += ` database:
1508
+ image: mysql:8.0
1509
+ container_name: ${projectName}-database
1510
+ restart: always
1511
+ environment:
1512
+ MYSQL_ROOT_PASSWORD: root
1513
+ ports:
1514
+ - "33060:3306"
1515
+ volumes:
1516
+ - mysql_data:/var/lib/mysql
1517
+ - ./db-init:/docker-entrypoint-initdb.d
1518
+ `;
1519
+ if (isMicroservices) {
1520
+ // Gateway service
1521
+ let gatewayEnv = ` DB_HOST: database
1522
+ DB_PORT: 3306
1523
+ DB_USER: root
1524
+ DB_PASSWORD: root
1525
+ DB_NAME: devstroupe
1526
+ JWT_SECRET: devstroupe-secret-key-12345
1527
+ `;
1528
+ let gatewayDependsOn = ` - database
1529
+ `;
1530
+ // Adiciona variáveis de microsserviços no gateway
1531
+ if (config.entities) {
1532
+ config.entities.forEach((entity, index) => {
1533
+ const port = 3001 + index;
1534
+ const nameUpper = entity.name.toUpperCase();
1535
+ gatewayEnv += ` ${nameUpper}_SERVICE_HOST: ${entity.name}-service
1536
+ ${nameUpper}_SERVICE_PORT: ${port}
1537
+ `;
1538
+ gatewayDependsOn += ` - ${entity.name}-service
1539
+ `;
1540
+ });
1541
+ }
1542
+ servicesYaml += `\n api-gateway:
1543
+ build:
1544
+ context: ./backend/api-gateway
1545
+ dockerfile: Dockerfile
1546
+ container_name: ${projectName}-api-gateway
1547
+ restart: always
1548
+ ports:
1549
+ - "13000:13000"
1550
+ environment:
1551
+ ${gatewayEnv} depends_on:
1552
+ ${gatewayDependsOn}`;
1553
+ // Containers de microsserviços
1554
+ if (config.entities) {
1555
+ config.entities.forEach((entity, index) => {
1556
+ const port = 3001 + index;
1557
+ servicesYaml += `\n\n ${entity.name}-service:
1558
+ build:
1559
+ context: ./backend/services/${entity.name}
1560
+ dockerfile: Dockerfile
1561
+ container_name: ${projectName}-${entity.name}-service
1562
+ restart: always
1563
+ ports:
1564
+ - "${port}0:${port}"
1565
+ environment:
1566
+ DB_HOST: database
1567
+ DB_PORT: 3306
1568
+ DB_USER: root
1569
+ DB_PASSWORD: root
1570
+ DB_NAME: devstroupe_${entity.name.toLowerCase()}
1571
+ SERVICE_PORT: ${port}
1572
+ depends_on:
1573
+ - database
1574
+ `;
1575
+ });
1576
+ }
1577
+ // Frontend service (depends on api-gateway)
1578
+ servicesYaml += `\n\n frontend:
1579
+ build:
1580
+ context: ./frontend
1581
+ dockerfile: Dockerfile
1582
+ container_name: ${projectName}-frontend
1583
+ restart: always
1584
+ ports:
1585
+ - "14200:80"
1586
+ depends_on:
1587
+ - api-gateway
1588
+ `;
1589
+ }
1590
+ else {
1591
+ // Monolith services
1592
+ servicesYaml += `\n backend:
1593
+ build:
1594
+ context: ./backend
1595
+ dockerfile: Dockerfile
1596
+ container_name: ${projectName}-backend
1597
+ restart: always
1598
+ ports:
1599
+ - "13000:13000"
1600
+ environment:
1601
+ DB_HOST: database
1602
+ DB_PORT: 3306
1603
+ DB_USER: root
1604
+ DB_PASSWORD: root
1605
+ DB_NAME: devstroupe
1606
+ JWT_SECRET: devstroupe-secret-key-12345
1607
+ depends_on:
1608
+ - database
1609
+
1610
+ frontend:
1611
+ build:
1612
+ context: ./frontend
1613
+ dockerfile: Dockerfile
1614
+ container_name: ${projectName}-frontend
1615
+ restart: always
1616
+ ports:
1617
+ - "14200:80"
1618
+ depends_on:
1619
+ - backend
1620
+ `;
1621
+ }
1622
+ const dockerComposeContent = `version: '3.8'
1623
+
1624
+ services:
1625
+ ${servicesYaml}
1626
+ volumes:
1627
+ mysql_data:
1628
+ `;
1629
+ fs.writeFileSync(path.join(projectRoot, 'docker-compose.yml'), dockerComposeContent, 'utf-8');
1630
+ console.log(`[Docker] docker-compose.yml regerado com sucesso.`);
1631
+ }
1632
+ // Comando new-project
1633
+ program
1634
+ .command('new-project <projectName>')
1635
+ .description('Cria um novo projeto com frontend Angular e backend NestJS pré-configurados')
1636
+ .option('--microservices', 'Cria o projeto utilizando arquitetura de microsserviços')
1637
+ .action((projectName, options) => {
1638
+ const projectRoot = path.resolve(process.cwd(), projectName);
1639
+ if (fs.existsSync(projectRoot) && fs.readdirSync(projectRoot).length > 0) {
1640
+ console.error(`[Erro] O diretório "${projectName}" já existe e não está vazio.`);
1641
+ process.exit(1);
1642
+ }
1643
+ const isMicroservices = !!options.microservices;
1644
+ console.log(`Criando novo projeto "${projectName}" (Modo: ${isMicroservices ? 'Microsserviços' : 'Monólito'}) em: ${projectRoot}...`);
1645
+ // Resolver caminhos de boilerplates
1646
+ let boilerplatesDir = path.resolve(__dirname, 'boilerplates');
1647
+ if (!fs.existsSync(boilerplatesDir)) {
1648
+ boilerplatesDir = path.resolve(__dirname, '../boilerplates');
1649
+ }
1650
+ if (!fs.existsSync(boilerplatesDir)) {
1651
+ boilerplatesDir = path.resolve(__dirname, '../../boilerplates');
1652
+ }
1653
+ const nestTemplateDir = path.join(boilerplatesDir, 'nest-template');
1654
+ const angularTemplateDir = path.join(boilerplatesDir, 'angular-template');
1655
+ if (!fs.existsSync(nestTemplateDir) || !fs.existsSync(angularTemplateDir)) {
1656
+ console.error(`[Erro] Não foi possível localizar os templates de boilerplate em: ${boilerplatesDir}`);
1657
+ process.exit(1);
1658
+ }
1659
+ try {
1660
+ // 1. Criar diretórios de destino
1661
+ fs.ensureDirSync(projectRoot);
1662
+ const frontendDest = path.join(projectRoot, 'frontend');
1663
+ let backendDest;
1664
+ if (isMicroservices) {
1665
+ backendDest = path.join(projectRoot, 'backend', 'api-gateway');
1666
+ fs.ensureDirSync(path.join(projectRoot, 'backend', 'services'));
1667
+ }
1668
+ else {
1669
+ backendDest = path.join(projectRoot, 'backend');
1670
+ }
1671
+ // 2. Copiar boilerplates
1672
+ console.log(`Copiando boilerplate do backend (${isMicroservices ? 'API Gateway' : 'NestJS Monólito'})...`);
1673
+ fs.copySync(nestTemplateDir, backendDest, { filter: createBoilerplateCopyFilter(nestTemplateDir) });
1674
+ console.log('Copiando boilerplate do frontend (Angular)...');
1675
+ fs.copySync(angularTemplateDir, frontendDest, { filter: createBoilerplateCopyFilter(angularTemplateDir) });
1676
+ // 2.1 Criar arquivos .env e .env.example no backend
1677
+ console.log('Gerando arquivos .env e .env.example no backend...');
1678
+ const envContent = `PORT=13000
1679
+ DB_HOST=localhost
1680
+ DB_PORT=3306
1681
+ DB_USER=root
1682
+ DB_PASSWORD=root
1683
+ DB_NAME=devstroupe
1684
+ `;
1685
+ fs.writeFileSync(path.join(backendDest, '.env'), envContent, 'utf-8');
1686
+ fs.writeFileSync(path.join(backendDest, '.env.example'), envContent, 'utf-8');
1687
+ // Patch local dependencies if in monorepo development
1688
+ patchLocalDependencies(backendDest, frontendDest, boilerplatesDir);
1689
+ // 3. Criar devstroupe.config.ts
1690
+ console.log('Gerando arquivo devstroupe.config.ts...');
1691
+ const configPath = path.join(projectRoot, 'devstroupe.config.ts');
1692
+ const designSystemConfig = {
1693
+ provider: 'spartan',
1694
+ spartanStyle: 'vega',
1695
+ spartanTheme: 'neutral',
1696
+ installAllPrimitives: true,
1697
+ includePlayground: true,
1698
+ };
1699
+ const configContent = `export default {
1700
+ projectName: '${projectName}',
1701
+ backendType: '${isMicroservices ? 'microservices' : 'monolith'}',
1702
+ backendPath: './backend',
1703
+ frontendPath: './frontend',
1704
+ designSystem: {
1705
+ provider: '${designSystemConfig.provider}',
1706
+ spartanStyle: '${designSystemConfig.spartanStyle}',
1707
+ spartanTheme: '${designSystemConfig.spartanTheme}',
1708
+ installAllPrimitives: ${designSystemConfig.installAllPrimitives},
1709
+ includePlayground: ${designSystemConfig.includePlayground}
1710
+ },
1711
+ entities: [
1712
+ {
1713
+ name: 'company',
1714
+ label: 'Empresa',
1715
+ tableName: 'companies',
1716
+ properties: [
1717
+ { name: 'name', type: 'string', required: true, label: 'Nome' },
1718
+ {
1719
+ name: 'cabins', type: 'relationship', required: false, label: 'Cabines',
1720
+ relation: {
1721
+ target: 'cabin', kind: 'one-to-many', foreignKey: 'company_id',
1722
+ inverseSide: 'company', control: 'table', embedded: true,
1723
+ columns: ['name', 'code', 'is_active'], lookup: { pageSize: 10 }
1724
+ }
1725
+ }
1726
+ ],
1727
+ crud: { generateFrontend: true, generateBackend: true }
1728
+ },
1729
+ {
1730
+ name: 'cabin',
1731
+ label: 'Cabine',
1732
+ tableName: 'cabins',
1733
+ properties: [
1734
+ { name: 'name', type: 'string', required: true, label: 'Nome' },
1735
+ { name: 'code', type: 'string', required: false, label: 'Código' },
1736
+ {
1737
+ name: 'company_id', type: 'relationship', required: true, label: 'Empresa',
1738
+ relation: {
1739
+ target: 'company', kind: 'many-to-one', relationName: 'company',
1740
+ inverseSide: 'cabins', control: 'select', displayField: 'name', onDelete: 'CASCADE',
1741
+ lookup: { mode: 'remote', pageSize: 20, minSearchLength: 0 }
1742
+ }
1743
+ },
1744
+ { name: 'is_active', type: 'boolean', required: true, label: 'Ativo', default: true }
1745
+ ],
1746
+ crud: {
1747
+ generateFrontend: true,
1748
+ generateBackend: true
1749
+ }
1750
+ }
1751
+ ]
1752
+ };
1753
+ `;
1754
+ fs.writeFileSync(configPath, configContent, 'utf-8');
1755
+ // 3.1 Criar docker-compose.yml e init.sql
1756
+ console.log('Gerando docker-compose.yml e base de dados...');
1757
+ const tempConfig = {
1758
+ projectName,
1759
+ backendType: isMicroservices ? 'microservices' : 'monolith',
1760
+ entities: [
1761
+ {
1762
+ name: 'cabin',
1763
+ tableName: 'cabins',
1764
+ }
1765
+ ]
1766
+ };
1767
+ generateDockerComposeAndDbInit(projectRoot, tempConfig);
1768
+ // 3.2 Criar Makefile na raiz
1769
+ console.log('Gerando arquivo Makefile na raiz...');
1770
+ const backendServiceName = isMicroservices ? 'api-gateway' : 'backend';
1771
+ const makefileContent = `.PHONY: up down restart build logs ps clean backend-shell frontend-shell db-shell help
1772
+
1773
+ DC = docker compose
1774
+
1775
+ help:
1776
+ @echo "Comandos disponíveis:"
1777
+ @echo " make up Inicia os containers em background"
1778
+ @echo " make down Para os containers e mantém os dados"
1779
+ @echo " make restart Reinicia os containers"
1780
+ @echo " make build Reconstrói as imagens Docker"
1781
+ @echo " make logs Exibe os logs em tempo real"
1782
+ @echo " make ps Exibe o status dos containers"
1783
+ @echo " make clean Para os containers e apaga todos os volumes (reseta o banco)"
1784
+ @echo " make backend-shell Abre o terminal sh do container do backend"
1785
+ @echo " make frontend-shell Abre o terminal sh do container do frontend"
1786
+ @echo " make db-shell Abre o cliente mysql no container do banco"
1787
+
1788
+ up:
1789
+ \$(DC) up -d
1790
+
1791
+ down:
1792
+ \$(DC) down
1793
+
1794
+ restart:
1795
+ \$(DC) restart
1796
+
1797
+ build:
1798
+ \$(DC) build
1799
+
1800
+ logs:
1801
+ \$(DC) logs -f
1802
+
1803
+ ps:
1804
+ \$(DC) ps
1805
+
1806
+ clean:
1807
+ \$(DC) down -v
1808
+
1809
+ backend-shell:
1810
+ \$(DC) exec ${backendServiceName} sh
1811
+
1812
+ frontend-shell:
1813
+ \$(DC) exec frontend sh
1814
+
1815
+ db-shell:
1816
+ \$(DC) exec database mysql -u root -p
1817
+
1818
+ # Evita que argumentos (ex: main, staging) sejam interpretados como target
1819
+ %:
1820
+ @:
1821
+ `;
1822
+ fs.writeFileSync(path.join(projectRoot, 'Makefile'), makefileContent, 'utf-8');
1823
+ // 4. Instalar dependências automaticamente
1824
+ try {
1825
+ console.log(`Instalando dependências no backend (${isMicroservices ? 'API Gateway' : 'Monólito'})...`);
1826
+ (0, child_process_1.execSync)('npm install', { cwd: backendDest, stdio: 'inherit' });
1827
+ }
1828
+ catch (err) {
1829
+ console.warn('[Aviso] Falha ao rodar "npm install" no backend. Instale as dependências manualmente.');
1830
+ }
1831
+ try {
1832
+ console.log('Instalando dependências no frontend...');
1833
+ (0, child_process_1.execSync)('npm install --legacy-peer-deps', { cwd: frontendDest, stdio: 'inherit' });
1834
+ }
1835
+ catch (err) {
1836
+ console.warn('[Aviso] Falha ao rodar "npm install" no frontend. Instale as dependências manualmente.');
1837
+ }
1838
+ // 5. Inicializar UI no frontend depois do install para disponibilizar @spartan-ng/cli
1839
+ console.log('Inicializando componentes de UI locais...');
1840
+ const generatedConfig = require(configPath).default ?? require(configPath);
1841
+ initUi(frontendDest, generatedConfig);
1842
+ console.log(`\n\x1b[32m[SUCESSO] Projeto "${projectName}" criado com sucesso!\x1b[0m`);
1843
+ console.log(`Para começar:`);
1844
+ console.log(` 1. Entre na pasta do projeto: cd ${projectName}`);
1845
+ console.log(` 2. Desenvolva sua aplicação com o DT-DevKit!`);
1846
+ }
1847
+ catch (err) {
1848
+ console.error('[Erro] Falha ao criar o novo projeto:', err);
1849
+ process.exit(1);
1850
+ }
1851
+ });
1852
+ // Comando add-ui
1853
+ program
1854
+ .command('add-ui <componentName>')
1855
+ .description('Gera um componente ou diretiva de UI local (Spartan ou Customizado DevKit) no frontend')
1856
+ .action((componentName) => {
1857
+ const configPath = path.join(process.cwd(), 'devstroupe.config.ts');
1858
+ if (!fs.existsSync(configPath)) {
1859
+ console.error('Arquivo devstroupe.config.ts não encontrado na raiz. Rode "devstroupe init" primeiro.');
1860
+ process.exit(1);
1861
+ }
1862
+ let config;
1863
+ try {
1864
+ const importedConfig = require(configPath);
1865
+ config = importedConfig.default || importedConfig;
1866
+ }
1867
+ catch (error) {
1868
+ console.error('Erro ao carregar devstroupe.config.ts:', error);
1869
+ process.exit(1);
1870
+ }
1871
+ const frontendRoot = path.resolve(process.cwd(), config.frontendPath || './frontend');
1872
+ if (!fs.existsSync(frontendRoot)) {
1873
+ console.error(`Diretório frontend não encontrado em: ${frontendRoot}.`);
1874
+ process.exit(1);
1875
+ }
1876
+ const targetDir = path.join(frontendRoot, 'src', 'app', 'shared', 'components', 'devkit');
1877
+ fs.ensureDirSync(targetDir);
1878
+ const compNameLower = componentName.toLowerCase();
1879
+ const pascalName = kebabToPascal(componentName);
1880
+ // Mapear e autoinstalar dependências do Spartan Brain correspondentes
1881
+ const spartanDeps = {
1882
+ dialog: '@spartan-ng/brain',
1883
+ sheet: '@spartan-ng/brain',
1884
+ tabs: '@spartan-ng/brain',
1885
+ accordion: '@spartan-ng/brain',
1886
+ 'alert-dialog': '@spartan-ng/brain',
1887
+ menu: '@spartan-ng/brain',
1888
+ 'dropdown-menu': '@spartan-ng/brain'
1889
+ };
1890
+ const depToInstall = spartanDeps[compNameLower];
1891
+ if (depToInstall) {
1892
+ const pkgJsonPath = path.join(frontendRoot, 'package.json');
1893
+ if (fs.existsSync(pkgJsonPath)) {
1894
+ const pkgJson = fs.readJsonSync(pkgJsonPath);
1895
+ const hasDep = (pkgJson.dependencies && pkgJson.dependencies[depToInstall]) ||
1896
+ (pkgJson.devDependencies && pkgJson.devDependencies[depToInstall]);
1897
+ if (!hasDep) {
1898
+ console.log(`[Dep] Instalando dependência necessária do Spartan: ${depToInstall}...`);
1899
+ const workspaceRoot = process.cwd();
1900
+ const isPnpm = fs.existsSync(path.join(workspaceRoot, 'pnpm-lock.yaml'));
1901
+ try {
1902
+ const { execSync } = require('child_process');
1903
+ if (isPnpm) {
1904
+ execSync(`pnpm --dir "${frontendRoot}" add ${depToInstall}`, { stdio: 'inherit' });
1905
+ }
1906
+ else {
1907
+ execSync(`npm --prefix "${frontendRoot}" install ${depToInstall} --legacy-peer-deps`, { stdio: 'inherit' });
1908
+ }
1909
+ console.log(`[Dep] ${depToInstall} instalado com sucesso!`);
1910
+ }
1911
+ catch (err) {
1912
+ console.warn(`[Aviso] Falha ao instalar automaticamente ${depToInstall}. Por favor, instale manualmente.`);
1913
+ }
1914
+ }
1915
+ }
1916
+ }
1917
+ if (compNameLower === 'badge') {
1918
+ const badgeDir = path.join(targetDir, 'ui-badge-helm');
1919
+ fs.ensureDirSync(badgeDir);
1920
+ const bdg = (0, ui_templates_1.devkitBadgeTemplate)();
1921
+ fs.writeFileSync(path.join(badgeDir, 'badge.component.ts'), bdg.ts, 'utf-8');
1922
+ console.log(`[UI] Componente devkit-badge injetado em: ${badgeDir}. O hlmBadge real vem de @spartan-ng/helm/badge.`);
1923
+ }
1924
+ else if (compNameLower === 'avatar') {
1925
+ const avatarDir = path.join(targetDir, 'ui-avatar-helm');
1926
+ fs.ensureDirSync(avatarDir);
1927
+ const avt = (0, ui_templates_1.devkitAvatarTemplate)();
1928
+ fs.writeFileSync(path.join(avatarDir, 'avatar.component.ts'), avt.ts, 'utf-8');
1929
+ console.log(`[UI] Componente devkit-avatar injetado em: ${avatarDir}`);
1930
+ }
1931
+ else if (['dialog', 'sheet', 'tabs', 'accordion', 'alert-dialog', 'menu', 'dropdown-menu'].includes(compNameLower)) {
1932
+ const primitiveName = compNameLower === 'menu' ? 'dropdown-menu' : compNameLower;
1933
+ const style = getCurrentSpartanStyle(frontendRoot, config);
1934
+ runSpartanPrimitiveGenerator(frontendRoot, style, primitiveName);
1935
+ console.log(`[UI] Primitive Spartan real disponível via import "@spartan-ng/helm/${primitiveName}". Nenhum wrapper fake foi gerado.`);
1936
+ }
1937
+ else {
1938
+ // Componente customizado livre!
1939
+ const customDir = path.join(targetDir, componentName);
1940
+ fs.ensureDirSync(customDir);
1941
+ const tsContent = `import { Component } from '@angular/core';
1942
+ import { CommonModule } from '@angular/common';
1943
+
1944
+ @Component({
1945
+ selector: 'devkit-${componentName}',
1946
+ standalone: true,
1947
+ imports: [CommonModule],
1948
+ templateUrl: './${componentName}.component.html',
1949
+ styleUrls: ['./${componentName}.component.css']
1950
+ })
1951
+ export class Devkit${pascalName}Component {}
1952
+ `;
1953
+ const htmlContent = `<div class="p-4 border border-dashed border-border rounded-lg bg-card text-card-foreground">
1954
+ <p class="text-sm font-medium">Componente Customizado: devkit-${componentName}</p>
1955
+ </div>
1956
+ `;
1957
+ const cssContent = `:host {
1958
+ display: block;
1959
+ }
1960
+ `;
1961
+ fs.writeFileSync(path.join(customDir, `${componentName}.component.ts`), tsContent, 'utf-8');
1962
+ fs.writeFileSync(path.join(customDir, `${componentName}.component.html`), htmlContent, 'utf-8');
1963
+ fs.writeFileSync(path.join(customDir, `${componentName}.component.css`), cssContent, 'utf-8');
1964
+ console.log(`[UI] Componente customizado do DevKit "devkit-${componentName}" criado com sucesso em: ${customDir}`);
1965
+ }
1966
+ });
1967
+ program.parse(process.argv);