generate-ui-cli 2.1.7 → 2.2.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.
package/README.md CHANGED
@@ -92,6 +92,7 @@ What happens after this command:
92
92
  - a typed API service
93
93
  - DTO/types files
94
94
  - route definitions
95
+ - `menu.json` and `menu.gen.ts` (if present in `generate-ui/`)
95
96
 
96
97
  What you should review now:
97
98
 
@@ -107,6 +108,52 @@ Defaults:
107
108
  - `--schemas` defaults to the last generated path (stored in `~/.generateui/config.json`), otherwise `./src/generate-ui` (or `./frontend/src/generate-ui` / `./generate-ui`)
108
109
  - `--features` defaults to `./src/app/features` when `./src/app` exists; otherwise it errors and asks for `--features`
109
110
 
111
+ ### generateui-config.json (optional)
112
+
113
+ GenerateUI creates a `generateui-config.json` at your project root on first `generate`. You can edit it to:
114
+
115
+ - inject a sidebar menu layout automatically (when `menu.autoInject` is not `false`)
116
+ - add a default redirect for `/` using `defaultRoute`
117
+ - show a custom app title in the menu (`appTitle`)
118
+
119
+ Example:
120
+
121
+ ```json
122
+ {
123
+ "appTitle": "Rick & Morty Admin",
124
+ "defaultRoute": "GetCharacter",
125
+ "menu": {
126
+ "autoInject": true
127
+ }
128
+ }
129
+ ```
130
+
131
+ Notes:
132
+ - If `menu.autoInject` is `false`, the menu layout is not injected.
133
+ - `defaultRoute` must match a path in `routes.gen.ts` (the same path used by the router).
134
+ - You can provide either the final route path or an `operationId`; the generator normalizes it to the correct path.
135
+ - You can override the menu by adding `menu.overrides.json` inside your `generate-ui/` folder (it replaces the generated menu entirely).
136
+
137
+ Example `menu.overrides.json`:
138
+
139
+ ```json
140
+ {
141
+ "groups": [
142
+ {
143
+ "id": "cadastros",
144
+ "label": "Cadastros",
145
+ "items": [
146
+ { "id": "GetCharacter", "label": "Personagens", "route": "getCharacter" },
147
+ { "id": "GetLocation", "label": "Localizações", "route": "getLocation" }
148
+ ]
149
+ }
150
+ ],
151
+ "ungrouped": [
152
+ { "id": "GetEpisode", "label": "Episódios", "route": "getEpisode" }
153
+ ]
154
+ }
155
+ ```
156
+
110
157
  Optional paths:
111
158
 
112
159
  ```bash
@@ -8,23 +8,49 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const feature_generator_1 = require("../generators/angular/feature.generator");
10
10
  const routes_generator_1 = require("../generators/angular/routes.generator");
11
+ const menu_generator_1 = require("../generators/angular/menu.generator");
11
12
  const telemetry_1 = require("../telemetry");
12
- const user_config_1 = require("../runtime/user-config");
13
+ const logger_1 = require("../runtime/logger");
13
14
  async function angular(options) {
14
15
  void (0, telemetry_1.trackCommand)('angular', options.telemetryEnabled);
15
16
  const featuresRoot = resolveFeaturesRoot(options.featuresPath);
16
17
  const schemasRoot = resolveSchemasRoot(options.schemasPath, featuresRoot);
18
+ (0, logger_1.logStep)(`Features output: ${featuresRoot}`);
19
+ (0, logger_1.logStep)(`Schemas input: ${schemasRoot}`);
17
20
  /**
18
21
  * Onde estão os schemas
19
22
  * Ex: generate-ui
20
23
  */
24
+ if (options.schemasPath && !fs_1.default.existsSync(schemasRoot)) {
25
+ fs_1.default.mkdirSync(schemasRoot, { recursive: true });
26
+ console.log(`ℹ Created generate-ui folder at ${schemasRoot}`);
27
+ }
21
28
  const overlaysDir = path_1.default.join(schemasRoot, 'overlays');
22
29
  if (!fs_1.default.existsSync(overlaysDir)) {
23
- throw new Error(`Overlays directory not found: ${overlaysDir}`);
30
+ const example = [
31
+ 'generate-ui angular \\',
32
+ ' --schemas /path/to/generate-ui \\',
33
+ ' --features /path/to/src/app/features'
34
+ ].join('\n');
35
+ throw new Error(`Overlays directory not found: ${overlaysDir}\n` +
36
+ 'Run `generate-ui generate --openapi <path> --output <schemas>` first to create overlays.\n' +
37
+ `Example:\n${example}`);
24
38
  }
39
+ (0, logger_1.logDebug)(`Overlays dir: ${overlaysDir}`);
25
40
  const screens = fs_1.default
26
41
  .readdirSync(overlaysDir)
27
42
  .filter(f => f.endsWith('.screen.json'));
43
+ (0, logger_1.logDebug)(`Screens found: ${screens.length}`);
44
+ if (screens.length === 0) {
45
+ const example = [
46
+ 'generate-ui angular \\',
47
+ ' --schemas /path/to/generate-ui \\',
48
+ ' --features /path/to/src/app/features'
49
+ ].join('\n');
50
+ throw new Error(`No .screen.json files found in: ${overlaysDir}\n` +
51
+ 'Run again with --schemas pointing to your generate-ui folder.\n' +
52
+ `Example:\n${example}`);
53
+ }
28
54
  /**
29
55
  * Onde gerar as features Angular
30
56
  */
@@ -36,19 +62,15 @@ async function angular(options) {
36
62
  routes.push(route);
37
63
  }
38
64
  (0, routes_generator_1.generateRoutes)(routes, featuresRoot, schemasRoot);
65
+ (0, menu_generator_1.generateMenu)(schemasRoot);
66
+ applyAppLayout(featuresRoot, schemasRoot);
39
67
  console.log(`✔ Angular features generated at ${featuresRoot}`);
68
+ (0, logger_1.logTip)('Run with --dev to see detailed logs and file paths.');
40
69
  }
41
70
  function resolveSchemasRoot(value, featuresRoot) {
42
71
  if (value) {
43
72
  return path_1.default.resolve(process.cwd(), value);
44
73
  }
45
- const config = (0, user_config_1.loadUserConfig)();
46
- if (config?.lastSchemasPath) {
47
- const resolved = path_1.default.resolve(config.lastSchemasPath);
48
- if (fs_1.default.existsSync(path_1.default.join(resolved, 'overlays'))) {
49
- return resolved;
50
- }
51
- }
52
74
  const inferred = inferSchemasRootFromFeatures(featuresRoot);
53
75
  if (inferred)
54
76
  return inferred;
@@ -71,9 +93,13 @@ function resolveFeaturesRoot(value) {
71
93
  return path_1.default.join(srcAppRoot, 'features');
72
94
  }
73
95
  function inferSchemasRootFromFeatures(featuresRoot) {
74
- const candidate = path_1.default.resolve(featuresRoot, '../../..', 'generate-ui');
75
- if (fs_1.default.existsSync(path_1.default.join(candidate, 'overlays'))) {
76
- return candidate;
96
+ const srcCandidate = path_1.default.resolve(featuresRoot, '..', '..', 'generate-ui');
97
+ if (fs_1.default.existsSync(path_1.default.join(srcCandidate, 'overlays'))) {
98
+ return srcCandidate;
99
+ }
100
+ const rootCandidate = path_1.default.resolve(featuresRoot, '../../..', 'generate-ui');
101
+ if (fs_1.default.existsSync(path_1.default.join(rootCandidate, 'overlays'))) {
102
+ return rootCandidate;
77
103
  }
78
104
  return null;
79
105
  }
@@ -87,3 +113,186 @@ function resolveDefaultSchemasRoot() {
87
113
  }
88
114
  return path_1.default.join(cwd, 'generate-ui');
89
115
  }
116
+ function applyAppLayout(featuresRoot, schemasRoot) {
117
+ const appRoot = path_1.default.resolve(featuresRoot, '..');
118
+ const configInfo = findConfig(appRoot);
119
+ const config = configInfo.config;
120
+ if (configInfo.configPath) {
121
+ (0, logger_1.logDebug)(`Config path: ${configInfo.configPath}`);
122
+ }
123
+ if (config) {
124
+ const resolvedTitle = config.appTitle ?? 'Generate UI';
125
+ const resolvedRoute = config.defaultRoute ?? '(not set)';
126
+ const resolvedInject = config.menu?.autoInject !== false;
127
+ console.log('✅ GenerateUI config detected');
128
+ console.log('');
129
+ console.log(` 🏷️ appTitle: "${resolvedTitle}"`);
130
+ console.log(` ➡️ defaultRoute: ${resolvedRoute}`);
131
+ console.log(` 🧭 menu.autoInject: ${resolvedInject}`);
132
+ console.log(' 🧩 menu overrides: edit generate-ui/menu.overrides.json to customize labels, groups, and order');
133
+ console.log(' (this file is created once and never overwritten)');
134
+ console.log('');
135
+ }
136
+ else {
137
+ console.log('ℹ️ No generateui-config.json found. Using defaults.');
138
+ console.log('');
139
+ console.log(' ✨ To customize, add generateui-config.json at your project root.');
140
+ console.log(' 🧩 To customize the menu, edit generate-ui/menu.overrides.json (created on first generate).');
141
+ console.log('');
142
+ }
143
+ if (config?.defaultRoute) {
144
+ injectDefaultRoute(appRoot, config.defaultRoute);
145
+ }
146
+ const autoInject = config?.menu?.autoInject !== false;
147
+ if (!autoInject)
148
+ return;
149
+ const appTitle = config?.appTitle || 'Generate UI';
150
+ injectMenuLayout(appRoot, appTitle, schemasRoot);
151
+ }
152
+ function findConfig(startDir) {
153
+ let dir = startDir;
154
+ let config = null;
155
+ let configPath = null;
156
+ const root = path_1.default.parse(dir).root;
157
+ while (true) {
158
+ const candidate = path_1.default.join(dir, 'generateui-config.json');
159
+ if (!config && fs_1.default.existsSync(candidate)) {
160
+ try {
161
+ config = JSON.parse(fs_1.default.readFileSync(candidate, 'utf-8'));
162
+ configPath = candidate;
163
+ }
164
+ catch {
165
+ config = null;
166
+ }
167
+ }
168
+ if (dir === root)
169
+ break;
170
+ dir = path_1.default.dirname(dir);
171
+ }
172
+ return { config, configPath };
173
+ }
174
+ function injectDefaultRoute(appRoot, value) {
175
+ const routesPath = path_1.default.join(appRoot, 'app.routes.ts');
176
+ if (!fs_1.default.existsSync(routesPath)) {
177
+ (0, logger_1.logDebug)(`Skip defaultRoute: ${routesPath} not found`);
178
+ return;
179
+ }
180
+ let content = fs_1.default.readFileSync(routesPath, 'utf-8');
181
+ const route = normalizeRoutePath(value);
182
+ const insertion = ` { path: '', pathMatch: 'full', redirectTo: '${route}' },\n`;
183
+ if (!content.trim().length) {
184
+ const template = `import { Routes } from '@angular/router'\nimport { generatedRoutes } from '../generate-ui/routes.gen'\n\nexport const routes: Routes = [\n${insertion} ...generatedRoutes\n]\n`;
185
+ fs_1.default.writeFileSync(routesPath, template);
186
+ (0, logger_1.logDebug)(`Default route injected (created): ${routesPath}`);
187
+ return;
188
+ }
189
+ if (!content.match(/export const routes\s*:\s*Routes\s*=/)) {
190
+ if (!content.match(/import\s+\{\s*Routes\s*\}\s+from\s+['"]@angular\/router['"]/)) {
191
+ content = `import { Routes } from '@angular/router'\n${content}`;
192
+ }
193
+ content = content.replace(/export const routes\s*=/, 'export const routes: Routes =');
194
+ }
195
+ content = content.replace(/export const routes\s*=\s*\[/, match => `${match}\n${insertion}`);
196
+ const emptyRedirect = /path:\s*['"]\s*['"]\s*,\s*pathMatch:\s*['"]full['"]\s*,\s*redirectTo:\s*['"]\s*['"]/;
197
+ if (emptyRedirect.test(content)) {
198
+ content = content.replace(emptyRedirect, `path: '', pathMatch: 'full', redirectTo: '${route}'`);
199
+ }
200
+ fs_1.default.writeFileSync(routesPath, content);
201
+ (0, logger_1.logDebug)(`Default route injected (updated): ${routesPath}`);
202
+ }
203
+ function injectMenuLayout(appRoot, appTitle, schemasRoot) {
204
+ const appHtmlPath = path_1.default.join(appRoot, 'app.html');
205
+ const appCssPath = path_1.default.join(appRoot, 'app.css');
206
+ const appTsPath = path_1.default.join(appRoot, 'app.ts');
207
+ if (!fs_1.default.existsSync(appHtmlPath) ||
208
+ !fs_1.default.existsSync(appCssPath) ||
209
+ !fs_1.default.existsSync(appTsPath)) {
210
+ (0, logger_1.logDebug)('Skip menu injection: app.html/app.css/app.ts not found');
211
+ return;
212
+ }
213
+ const htmlRaw = fs_1.default.readFileSync(appHtmlPath, 'utf-8');
214
+ if (htmlRaw.includes('<ui-menu')) {
215
+ let updatedHtml = htmlRaw;
216
+ updatedHtml = updatedHtml.replace(/<ui-menu(?![^>]*\[\s*title\s*\])[\\s>]/, '<ui-menu [title]="appTitle()">');
217
+ if (updatedHtml !== htmlRaw) {
218
+ fs_1.default.writeFileSync(appHtmlPath, updatedHtml);
219
+ (0, logger_1.logDebug)(`Updated ui-menu title binding: ${appHtmlPath}`);
220
+ }
221
+ }
222
+ else {
223
+ const normalized = htmlRaw.replace(/\s+/g, '');
224
+ const isDefaultOutlet = normalized === '<router-outlet></router-outlet>' ||
225
+ normalized === '<router-outlet/>' ||
226
+ normalized === '<router-outlet/>';
227
+ if (!isDefaultOutlet)
228
+ return;
229
+ const newHtml = `<div class="app-shell">\n <ui-menu [title]="appTitle()"></ui-menu>\n <main class="app-content">\n <router-outlet></router-outlet>\n </main>\n</div>\n`;
230
+ fs_1.default.writeFileSync(appHtmlPath, newHtml);
231
+ (0, logger_1.logDebug)(`Injected menu layout into: ${appHtmlPath}`);
232
+ }
233
+ const cssRaw = fs_1.default.readFileSync(appCssPath, 'utf-8');
234
+ if (!cssRaw.includes('.app-shell')) {
235
+ const shellCss = `:host {\n display: block;\n min-height: 100vh;\n color: #0f172a;\n background:\n radial-gradient(circle at top left, rgba(14, 116, 144, 0.14), transparent 55%),\n radial-gradient(circle at bottom right, rgba(59, 130, 246, 0.12), transparent 50%),\n linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);\n}\n\n.app-shell {\n display: grid;\n grid-template-columns: 260px 1fr;\n gap: 24px;\n padding: 24px;\n align-items: start;\n}\n\n.app-content {\n min-width: 0;\n}\n\n@media (max-width: 900px) {\n .app-shell {\n grid-template-columns: 1fr;\n }\n}\n`;
236
+ fs_1.default.writeFileSync(appCssPath, cssRaw.trim().length ? `${cssRaw.trim()}\n\n${shellCss}` : shellCss);
237
+ (0, logger_1.logDebug)(`Injected menu shell styles into: ${appCssPath}`);
238
+ }
239
+ let tsRaw = fs_1.default.readFileSync(appTsPath, 'utf-8');
240
+ if (!tsRaw.includes('UiMenuComponent')) {
241
+ tsRaw = tsRaw.replace(/import\s+\{\s*([^}]+)\s*\}\s+from\s+['"]@angular\/router['"];/, (match) => `${match}\nimport { UiMenuComponent } from './ui/ui-menu/ui-menu.component';`);
242
+ }
243
+ if (tsRaw.includes('imports: [')) {
244
+ tsRaw = tsRaw.replace(/imports:\s*\[/, match => `${match}RouterOutlet, UiMenuComponent, `);
245
+ tsRaw = tsRaw.replace(/UiMenuComponent,\s*UiMenuComponent,\s*/g, 'UiMenuComponent, ');
246
+ tsRaw = tsRaw.replace(/RouterOutlet,\s*RouterOutlet,\s*/g, 'RouterOutlet, ');
247
+ tsRaw = tsRaw.replace(/UiMenuComponent,\s*RouterOutlet,\s*UiMenuComponent,/g, 'UiMenuComponent, ');
248
+ }
249
+ if (tsRaw.includes('appTitle')) {
250
+ tsRaw = tsRaw.replace(/appTitle\s*=\s*signal\('([^']*)'\)/, `appTitle = signal('${escapeString(appTitle)}')`);
251
+ }
252
+ else {
253
+ if (!tsRaw.match(/import\s+\{\s*[^}]*\bsignal\b[^}]*\}\s+from\s+['"]@angular\/core['"]/)) {
254
+ tsRaw = tsRaw.replace(/import\s+\{\s*([^}]+)\s*\}\s+from\s+['"]@angular\/core['"];?/, (match, imports) => {
255
+ if (imports.includes('signal'))
256
+ return match;
257
+ return `import { ${imports.trim()}, signal } from '@angular/core';`;
258
+ });
259
+ }
260
+ tsRaw = tsRaw.replace(/export class App\s*\{\s*/, match => `${match}\n protected readonly appTitle = signal('${escapeString(appTitle)}');\n`);
261
+ }
262
+ // Remove legacy runtime config loader if present.
263
+ if (tsRaw.includes('loadRuntimeConfig')) {
264
+ tsRaw = tsRaw.replace(/\\s*constructor\\(\\)\\s*\\{[\\s\\S]*?\\}\\s*/m, '\n');
265
+ tsRaw = tsRaw.replace(/\\s*private\\s+loadRuntimeConfig\\(\\)\\s*\\{[\\s\\S]*?\\}\\s*/m, '\n');
266
+ }
267
+ fs_1.default.writeFileSync(appTsPath, tsRaw);
268
+ (0, logger_1.logDebug)(`Updated app title/menu imports: ${appTsPath}`);
269
+ const menuComponentPath = path_1.default.join(appRoot, 'ui', 'ui-menu', 'ui-menu.component.ts');
270
+ if (fs_1.default.existsSync(menuComponentPath)) {
271
+ // Touch to keep consistent in case it was generated before config title existed.
272
+ void schemasRoot;
273
+ }
274
+ }
275
+ function escapeString(value) {
276
+ return String(value).replace(/'/g, "\\'");
277
+ }
278
+ function normalizeRoutePath(value) {
279
+ const trimmed = String(value ?? '').trim();
280
+ if (!trimmed)
281
+ return trimmed;
282
+ if (trimmed.includes('/'))
283
+ return trimmed.replace(/^\//, '');
284
+ const pascal = toPascalCase(trimmed);
285
+ return toRouteSegment(pascal);
286
+ }
287
+ function toRouteSegment(value) {
288
+ if (!value)
289
+ return value;
290
+ return value[0].toLowerCase() + value.slice(1);
291
+ }
292
+ function toPascalCase(value) {
293
+ return String(value)
294
+ .split(/[^a-zA-Z0-9]+/)
295
+ .filter(Boolean)
296
+ .map(part => part[0].toUpperCase() + part.slice(1))
297
+ .join('');
298
+ }
@@ -13,6 +13,7 @@ const permissions_1 = require("../license/permissions");
13
13
  const device_1 = require("../license/device");
14
14
  const telemetry_1 = require("../telemetry");
15
15
  const user_config_1 = require("../runtime/user-config");
16
+ const logger_1 = require("../runtime/logger");
16
17
  async function generate(options) {
17
18
  void (0, telemetry_1.trackCommand)('generate', options.telemetryEnabled);
18
19
  /**
@@ -20,16 +21,21 @@ async function generate(options) {
20
21
  * Ex: /Users/.../generateui-playground/realWorldOpenApi.yaml
21
22
  */
22
23
  const openApiPath = path_1.default.resolve(process.cwd(), options.openapi);
24
+ (0, logger_1.logStep)(`OpenAPI: ${openApiPath}`);
23
25
  /**
24
26
  * Raiz do playground (onde está o YAML)
25
27
  */
26
28
  const projectRoot = path_1.default.dirname(openApiPath);
29
+ (0, logger_1.logDebug)(`Project root: ${projectRoot}`);
27
30
  /**
28
31
  * Onde o Angular consome os arquivos
29
32
  */
30
33
  const generateUiRoot = resolveGenerateUiRoot(projectRoot, options.output);
34
+ (0, logger_1.logStep)(`Schemas output: ${generateUiRoot}`);
31
35
  const generatedDir = path_1.default.join(generateUiRoot, 'generated');
32
36
  const overlaysDir = path_1.default.join(generateUiRoot, 'overlays');
37
+ (0, logger_1.logDebug)(`Generated dir: ${generatedDir}`);
38
+ (0, logger_1.logDebug)(`Overlays dir: ${overlaysDir}`);
33
39
  (0, user_config_1.updateUserConfig)(config => ({
34
40
  ...config,
35
41
  lastSchemasPath: generateUiRoot
@@ -43,17 +49,20 @@ async function generate(options) {
43
49
  const usedOperationIds = new Set();
44
50
  const permissions = await (0, permissions_1.getPermissions)();
45
51
  const device = (0, device_1.loadDeviceIdentity)();
52
+ (0, logger_1.logDebug)(`License: maxGenerations=${permissions.features.maxGenerations}, overrides=${permissions.features.uiOverrides}, safeRegen=${permissions.features.safeRegeneration}`);
46
53
  if (permissions.features.maxGenerations > -1 &&
47
54
  device.freeGenerationsUsed >= permissions.features.maxGenerations) {
48
55
  throw new Error('🔒 Você já utilizou sua geração gratuita.\n' +
49
56
  'O plano Dev libera gerações ilimitadas, regeneração segura e UI inteligente.\n' +
50
- '👉 Execute `generate-ui login` para continuar.');
57
+ '👉 Execute `generate-ui login` para continuar.\n' +
58
+ 'Se você já fez login e ainda vê esta mensagem, tente novamente com a mesma versão do CLI e verifique a conexão com a API.');
51
59
  }
52
60
  /**
53
61
  * Parse do OpenAPI (já com $refs resolvidos)
54
62
  */
55
63
  const api = await (0, load_openapi_1.loadOpenApi)(openApiPath);
56
64
  const paths = api.paths ?? {};
65
+ (0, logger_1.logDebug)(`OpenAPI paths: ${Object.keys(paths).length}`);
57
66
  /**
58
67
  * Itera por todos os endpoints
59
68
  */
@@ -117,16 +126,58 @@ async function generate(options) {
117
126
  */
118
127
  routes.push({
119
128
  path: operationId,
120
- operationId
129
+ operationId,
130
+ label: toLabel(screenSchema.entity
131
+ ? String(screenSchema.entity)
132
+ : operationId),
133
+ group: inferRouteGroup(op, pathKey)
121
134
  });
122
135
  console.log(`✔ Generated ${operationId}`);
123
136
  }
124
137
  }
138
+ (0, logger_1.logStep)(`Screens generated: ${routes.length}`);
125
139
  /**
126
140
  * 4️⃣ Gera arquivo de rotas
127
141
  */
128
142
  const routesPath = path_1.default.join(generateUiRoot, 'routes.json');
129
143
  fs_1.default.writeFileSync(routesPath, JSON.stringify(routes, null, 2));
144
+ (0, logger_1.logDebug)(`Routes written: ${routesPath}`);
145
+ /**
146
+ * 4.3️⃣ Gera generateui-config.json (não sobrescreve)
147
+ */
148
+ const configPath = path_1.default.join(generateUiRoot, '..', '..', 'generateui-config.json');
149
+ if (!fs_1.default.existsSync(configPath)) {
150
+ const defaultConfig = {
151
+ appTitle: 'Generate UI',
152
+ defaultRoute: '',
153
+ menu: {
154
+ autoInject: true
155
+ }
156
+ };
157
+ fs_1.default.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
158
+ (0, logger_1.logDebug)(`Config created: ${configPath}`);
159
+ }
160
+ else {
161
+ (0, logger_1.logDebug)(`Config found: ${configPath}`);
162
+ }
163
+ /**
164
+ * 4.1️⃣ Gera menu inicial (override possível via menu.overrides.json)
165
+ */
166
+ const menuPath = path_1.default.join(generateUiRoot, 'menu.json');
167
+ const menu = buildMenuFromRoutes(routes);
168
+ fs_1.default.writeFileSync(menuPath, JSON.stringify(menu, null, 2));
169
+ (0, logger_1.logDebug)(`Menu written: ${menuPath}`);
170
+ /**
171
+ * 4.2️⃣ Gera menu.overrides.json (não sobrescreve)
172
+ */
173
+ const menuOverridesPath = path_1.default.join(generateUiRoot, 'menu.overrides.json');
174
+ if (!fs_1.default.existsSync(menuOverridesPath)) {
175
+ fs_1.default.writeFileSync(menuOverridesPath, JSON.stringify(menu, null, 2));
176
+ (0, logger_1.logDebug)(`Menu overrides created: ${menuOverridesPath}`);
177
+ }
178
+ else {
179
+ (0, logger_1.logDebug)(`Menu overrides found: ${menuOverridesPath}`);
180
+ }
130
181
  /**
131
182
  * 5️⃣ Remove overlays órfãos (endpoint removido)
132
183
  */
@@ -146,6 +197,15 @@ async function generate(options) {
146
197
  (0, device_1.incrementFreeGeneration)();
147
198
  }
148
199
  console.log('✔ Routes generated');
200
+ console.log('');
201
+ console.log('🎉 Next steps');
202
+ console.log(' 1) Generate Angular code:');
203
+ console.log(' generate-ui angular --schemas <your-generate-ui> --features <your-app>/src/app/features');
204
+ console.log(' 2) Customize screens in generate-ui/overlays/');
205
+ console.log(' 3) Customize menu in generate-ui/menu.overrides.json (created once, never overwritten)');
206
+ console.log(' 4) Edit generateui-config.json to set appTitle/defaultRoute/menu.autoInject');
207
+ console.log('');
208
+ (0, logger_1.logTip)('Run with --dev to see detailed logs and file paths.');
149
209
  }
150
210
  function resolveGenerateUiRoot(projectRoot, output) {
151
211
  if (output) {
@@ -199,6 +259,64 @@ function buildOperationId(method, pathKey, usedOperationIds) {
199
259
  usedOperationIds.add(candidate);
200
260
  return candidate;
201
261
  }
262
+ function inferRouteGroup(op, pathKey) {
263
+ const tag = Array.isArray(op?.tags) && op.tags.length
264
+ ? String(op.tags[0]).trim()
265
+ : '';
266
+ if (tag)
267
+ return tag;
268
+ const segment = String(pathKey || '')
269
+ .split('/')
270
+ .map(part => part.trim())
271
+ .filter(Boolean)
272
+ .find(part => !part.startsWith('{') && !part.endsWith('}'));
273
+ return segment ? segment : null;
274
+ }
275
+ function buildMenuFromRoutes(routes) {
276
+ const groups = [];
277
+ const ungrouped = [];
278
+ const groupMap = new Map();
279
+ for (const route of routes) {
280
+ const item = {
281
+ id: route.operationId,
282
+ label: toLabel(route.label || route.operationId),
283
+ route: route.path
284
+ };
285
+ const rawGroup = route.group ? String(route.group) : '';
286
+ if (!rawGroup) {
287
+ ungrouped.push(item);
288
+ continue;
289
+ }
290
+ const groupId = toKebab(rawGroup);
291
+ let group = groupMap.get(groupId);
292
+ if (!group) {
293
+ group = {
294
+ id: groupId,
295
+ label: toLabel(rawGroup),
296
+ items: []
297
+ };
298
+ groupMap.set(groupId, group);
299
+ groups.push(group);
300
+ }
301
+ group.items.push(item);
302
+ }
303
+ return {
304
+ groups,
305
+ ungrouped
306
+ };
307
+ }
308
+ function toKebab(value) {
309
+ return String(value)
310
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
311
+ .replace(/[_\s]+/g, '-')
312
+ .toLowerCase();
313
+ }
314
+ function toLabel(value) {
315
+ return String(value)
316
+ .replace(/[_-]/g, ' ')
317
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
318
+ .replace(/\b\w/g, char => char.toUpperCase());
319
+ }
202
320
  function httpVerbToPrefix(verb) {
203
321
  switch (verb) {
204
322
  case 'get':
@@ -6,25 +6,30 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.login = login;
7
7
  const http_1 = __importDefault(require("http"));
8
8
  const url_1 = require("url");
9
- const promises_1 = require("readline/promises");
9
+ const child_process_1 = require("child_process");
10
10
  const config_1 = require("../runtime/config");
11
11
  const user_config_1 = require("../runtime/user-config");
12
12
  const open_browser_1 = require("../runtime/open-browser");
13
13
  const token_1 = require("../license/token");
14
14
  const permissions_1 = require("../license/permissions");
15
15
  const telemetry_1 = require("../telemetry");
16
+ const logger_1 = require("../runtime/logger");
16
17
  const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
17
18
  async function login(options) {
18
19
  void (0, telemetry_1.trackCommand)('login', options.telemetryEnabled);
20
+ (0, logger_1.logStep)('Starting login flow');
19
21
  const token = await waitForLogin();
20
22
  (0, token_1.saveToken)(token);
23
+ (0, logger_1.logDebug)('Token saved');
24
+ let permissionsLoaded = false;
21
25
  try {
22
26
  await (0, permissions_1.fetchPermissions)();
27
+ permissionsLoaded = true;
23
28
  }
24
29
  catch {
25
- // Cached permissions will be refreshed on next online command.
30
+ console.warn('⚠ Não foi possível validar a licença agora. Verifique sua conexão e rode o comando novamente se necessário.');
26
31
  }
27
- const email = await promptEmail();
32
+ const email = resolveLoginEmail();
28
33
  if (email) {
29
34
  (0, user_config_1.updateUserConfig)((config) => ({
30
35
  ...config,
@@ -32,22 +37,27 @@ async function login(options) {
32
37
  }));
33
38
  }
34
39
  await (0, telemetry_1.trackLogin)(email, options.telemetryEnabled);
35
- console.log('✔ Login completo');
40
+ console.log(permissionsLoaded
41
+ ? '✔ Login completo'
42
+ : '✔ Login completo (verificação pendente)');
36
43
  }
37
- async function promptEmail() {
38
- if (!process.stdin.isTTY)
39
- return null;
40
- const rl = (0, promises_1.createInterface)({
41
- input: process.stdin,
42
- output: process.stdout
43
- });
44
+ function resolveLoginEmail() {
45
+ const envEmail = process.env.GIT_AUTHOR_EMAIL ||
46
+ process.env.GIT_COMMITTER_EMAIL ||
47
+ process.env.EMAIL;
48
+ if (envEmail && envEmail.trim().length) {
49
+ return envEmail.trim();
50
+ }
44
51
  try {
45
- const value = await rl.question('Email (optional): ');
46
- const trimmed = value.trim();
47
- return trimmed.length ? trimmed : null;
52
+ const output = (0, child_process_1.execSync)('git config --get user.email', {
53
+ stdio: ['ignore', 'pipe', 'ignore']
54
+ })
55
+ .toString()
56
+ .trim();
57
+ return output.length ? output : null;
48
58
  }
49
- finally {
50
- rl.close();
59
+ catch {
60
+ return null;
51
61
  }
52
62
  }
53
63
  async function waitForLogin() {
@@ -68,7 +78,7 @@ async function waitForLogin() {
68
78
  res.end('Missing access token');
69
79
  return;
70
80
  }
71
- const expiresAt = expiresAtParam ||
81
+ const expiresAt = normalizeExpiresAt(expiresAtParam) ||
72
82
  new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
73
83
  res.writeHead(200, { 'Content-Type': 'text/html' });
74
84
  res.end(`<!doctype html>
@@ -158,7 +168,24 @@ async function waitForLogin() {
158
168
  url.searchParams.set('api_base', (0, config_1.getApiBaseUrl)());
159
169
  loginUrl = url.toString();
160
170
  console.log(`Open this URL to finish login: ${loginUrl}`);
171
+ (0, logger_1.logDebug)(`Login callback listening on ${redirectUri}`);
161
172
  (0, open_browser_1.openBrowser)(loginUrl);
162
173
  });
163
174
  });
164
175
  }
176
+ function normalizeExpiresAt(value) {
177
+ if (!value)
178
+ return null;
179
+ const trimmed = value.trim();
180
+ if (!trimmed.length)
181
+ return null;
182
+ const asNumber = Number(trimmed);
183
+ if (Number.isFinite(asNumber)) {
184
+ const ms = asNumber < 1e12 ? asNumber * 1000 : asNumber;
185
+ return new Date(ms).toISOString();
186
+ }
187
+ const parsed = new Date(trimmed);
188
+ if (Number.isNaN(parsed.getTime()))
189
+ return null;
190
+ return parsed.toISOString();
191
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generatedRoutes = void 0;
4
+ exports.generatedRoutes = [];