generate-ui-cli 2.1.7 → 2.3.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.
@@ -10,26 +10,40 @@ const load_openapi_1 = require("../openapi/load-openapi");
10
10
  const screen_generator_1 = require("../generators/screen.generator");
11
11
  const screen_merge_1 = require("../generators/screen.merge");
12
12
  const permissions_1 = require("../license/permissions");
13
- const device_1 = require("../license/device");
14
13
  const telemetry_1 = require("../telemetry");
15
14
  const user_config_1 = require("../runtime/user-config");
15
+ const logger_1 = require("../runtime/logger");
16
+ const project_config_1 = require("../runtime/project-config");
16
17
  async function generate(options) {
17
18
  void (0, telemetry_1.trackCommand)('generate', options.telemetryEnabled);
19
+ const projectConfig = (0, project_config_1.findProjectConfig)(process.cwd());
20
+ const configuredOpenApi = (0, project_config_1.pickConfiguredPath)(projectConfig.config, 'openapi');
21
+ const configuredOutput = (0, project_config_1.pickConfiguredPath)(projectConfig.config, 'output') ??
22
+ (0, project_config_1.pickConfiguredPath)(projectConfig.config, 'schemas');
23
+ const openApiPath = (0, project_config_1.resolveOptionalPath)(options.openapi, configuredOpenApi, projectConfig.configPath);
24
+ if (!openApiPath) {
25
+ throw new Error('Missing OpenAPI file.\n' +
26
+ 'Use --openapi <path> or set "openapi" (or "paths.openapi") in generateui-config.json.');
27
+ }
18
28
  /**
19
29
  * Caminho absoluto do OpenAPI (YAML)
20
30
  * Ex: /Users/.../generateui-playground/realWorldOpenApi.yaml
21
31
  */
22
- const openApiPath = path_1.default.resolve(process.cwd(), options.openapi);
32
+ (0, logger_1.logStep)(`OpenAPI: ${openApiPath}`);
23
33
  /**
24
34
  * Raiz do playground (onde está o YAML)
25
35
  */
26
36
  const projectRoot = path_1.default.dirname(openApiPath);
37
+ (0, logger_1.logDebug)(`Project root: ${projectRoot}`);
27
38
  /**
28
39
  * Onde o Angular consome os arquivos
29
40
  */
30
- const generateUiRoot = resolveGenerateUiRoot(projectRoot, options.output);
41
+ const generateUiRoot = resolveGenerateUiRoot(projectRoot, (0, project_config_1.resolveOptionalPath)(options.output, configuredOutput, projectConfig.configPath) ?? undefined);
42
+ (0, logger_1.logStep)(`Schemas output: ${generateUiRoot}`);
31
43
  const generatedDir = path_1.default.join(generateUiRoot, 'generated');
32
44
  const overlaysDir = path_1.default.join(generateUiRoot, 'overlays');
45
+ (0, logger_1.logDebug)(`Generated dir: ${generatedDir}`);
46
+ (0, logger_1.logDebug)(`Overlays dir: ${overlaysDir}`);
33
47
  (0, user_config_1.updateUserConfig)(config => ({
34
48
  ...config,
35
49
  lastSchemasPath: generateUiRoot
@@ -41,23 +55,26 @@ async function generate(options) {
41
55
  */
42
56
  const routes = [];
43
57
  const usedOperationIds = new Set();
58
+ const resourceMap = new Map();
59
+ const screenByOpId = new Map();
60
+ const viewDefaults = [];
44
61
  const permissions = await (0, permissions_1.getPermissions)();
45
- const device = (0, device_1.loadDeviceIdentity)();
46
- if (permissions.features.maxGenerations > -1 &&
47
- device.freeGenerationsUsed >= permissions.features.maxGenerations) {
48
- throw new Error('🔒 Você já utilizou sua geração gratuita.\n' +
49
- 'O plano Dev libera gerações ilimitadas, regeneração segura e UI inteligente.\n' +
50
- '👉 Execute `generate-ui login` para continuar.');
62
+ const subscriptionReason = normalizeSubscriptionReason(permissions.subscription.reason);
63
+ (0, logger_1.logDebug)(`License: status=${permissions.subscription.status}, overrides=${permissions.features.uiOverrides}, safeRegen=${permissions.features.safeRegeneration}, intelligent=${permissions.features.intelligentGeneration}`);
64
+ if (subscriptionReason) {
65
+ console.log(`ℹ Subscription: ${subscriptionReason}`);
51
66
  }
52
67
  /**
53
68
  * Parse do OpenAPI (já com $refs resolvidos)
54
69
  */
55
70
  const api = await (0, load_openapi_1.loadOpenApi)(openApiPath);
56
71
  const paths = api.paths ?? {};
72
+ (0, logger_1.logDebug)(`OpenAPI paths: ${Object.keys(paths).length}`);
57
73
  /**
58
74
  * Itera por todos os endpoints
59
75
  */
60
76
  const operationIds = new Set();
77
+ const operationFileNames = new Set();
61
78
  for (const [pathKey, pathItem] of Object.entries(paths)) {
62
79
  for (const [method, rawOp] of Object.entries(pathItem)) {
63
80
  const op = rawOp;
@@ -66,6 +83,8 @@ async function generate(options) {
66
83
  const operationId = op.operationId ||
67
84
  buildOperationId(method.toLowerCase(), pathKey, usedOperationIds);
68
85
  operationIds.add(operationId);
86
+ operationFileNames.add(toSafeFileName(operationId));
87
+ recordResourceOp(resourceMap, pathKey, method.toLowerCase(), operationId, op);
69
88
  const endpoint = {
70
89
  operationId,
71
90
  path: pathKey,
@@ -77,7 +96,8 @@ async function generate(options) {
77
96
  * Gera o ScreenSchema completo
78
97
  */
79
98
  const screenSchema = (0, screen_generator_1.generateScreen)(endpoint, api);
80
- const fileName = `${operationId}.screen.json`;
99
+ screenByOpId.set(operationId, screenSchema);
100
+ const fileName = `${toSafeFileName(operationId)}.screen.json`;
81
101
  /**
82
102
  * 1️⃣ generated → SEMPRE sobrescrito (base técnica)
83
103
  */
@@ -117,16 +137,124 @@ async function generate(options) {
117
137
  */
118
138
  routes.push({
119
139
  path: operationId,
120
- operationId
140
+ operationId,
141
+ label: toLabel(screenSchema.entity
142
+ ? String(screenSchema.entity)
143
+ : operationId),
144
+ group: inferRouteGroup(op, pathKey)
145
+ });
146
+ viewDefaults.push({
147
+ key: operationId,
148
+ view: 'list'
121
149
  });
122
150
  console.log(`✔ Generated ${operationId}`);
123
151
  }
124
152
  }
153
+ const canOverride = permissions.features.uiOverrides;
154
+ const canRegenerateSafely = permissions.features.safeRegeneration;
155
+ const intelligentEnabled = Boolean(permissions.features.intelligentGeneration);
156
+ if (intelligentEnabled) {
157
+ const adminSchemas = buildAdminSchemas(resourceMap, usedOperationIds, screenByOpId);
158
+ for (const admin of adminSchemas) {
159
+ const fileName = `${toSafeFileName(admin.api.operationId)}.screen.json`;
160
+ const generatedPath = path_1.default.join(generatedDir, fileName);
161
+ fs_1.default.writeFileSync(generatedPath, JSON.stringify(admin, null, 2));
162
+ const overlayPath = path_1.default.join(overlaysDir, fileName);
163
+ if (canOverride && canRegenerateSafely) {
164
+ const overlay = fs_1.default.existsSync(overlayPath)
165
+ ? JSON.parse(fs_1.default.readFileSync(overlayPath, 'utf-8'))
166
+ : null;
167
+ const merged = (0, screen_merge_1.mergeScreen)(admin, overlay, null, {
168
+ openapiVersion: api?.info?.version || 'unknown',
169
+ debug: options.debug
170
+ });
171
+ fs_1.default.writeFileSync(overlayPath, JSON.stringify(merged.screen, null, 2));
172
+ }
173
+ else if (!fs_1.default.existsSync(overlayPath)) {
174
+ fs_1.default.writeFileSync(overlayPath, JSON.stringify(admin, null, 2));
175
+ }
176
+ routes.push({
177
+ path: admin.api.operationId,
178
+ operationId: admin.api.operationId,
179
+ label: admin.meta?.intelligent?.label,
180
+ group: admin.meta?.intelligent?.group ?? null
181
+ });
182
+ viewDefaults.push({
183
+ key: admin.api.operationId,
184
+ view: 'cards'
185
+ });
186
+ if (admin.meta?.intelligent?.listOperationId) {
187
+ viewDefaults.push({
188
+ key: admin.meta.intelligent.listOperationId,
189
+ view: 'list'
190
+ });
191
+ }
192
+ operationIds.add(admin.api.operationId);
193
+ operationFileNames.add(toSafeFileName(admin.api.operationId));
194
+ console.log(`✨ Generated ${admin.api.operationId}`);
195
+ }
196
+ }
197
+ (0, logger_1.logStep)(`Screens generated: ${routes.length}`);
125
198
  /**
126
199
  * 4️⃣ Gera arquivo de rotas
127
200
  */
128
201
  const routesPath = path_1.default.join(generateUiRoot, 'routes.json');
129
202
  fs_1.default.writeFileSync(routesPath, JSON.stringify(routes, null, 2));
203
+ (0, logger_1.logDebug)(`Routes written: ${routesPath}`);
204
+ /**
205
+ * 4.3️⃣ Gera generateui-config.json (não sobrescreve)
206
+ */
207
+ const configPath = path_1.default.join(generateUiRoot, '..', '..', 'generateui-config.json');
208
+ let configPayload = null;
209
+ if (!fs_1.default.existsSync(configPath)) {
210
+ configPayload = {
211
+ appTitle: 'Generate UI',
212
+ defaultRoute: '',
213
+ menu: {
214
+ autoInject: true
215
+ },
216
+ views: {}
217
+ };
218
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configPayload, null, 2));
219
+ (0, logger_1.logDebug)(`Config created: ${configPath}`);
220
+ }
221
+ else {
222
+ try {
223
+ configPayload = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
224
+ }
225
+ catch {
226
+ configPayload = null;
227
+ }
228
+ (0, logger_1.logDebug)(`Config found: ${configPath}`);
229
+ }
230
+ if (configPayload && viewDefaults.length) {
231
+ configPayload.views = configPayload.views || {};
232
+ for (const entry of viewDefaults) {
233
+ if (!configPayload.views[entry.key]) {
234
+ configPayload.views[entry.key] = entry.view;
235
+ }
236
+ }
237
+ fs_1.default.writeFileSync(configPath, JSON.stringify(configPayload, null, 2));
238
+ (0, logger_1.logDebug)(`Config views updated: ${configPath}`);
239
+ }
240
+ /**
241
+ * 4.1️⃣ Gera menu inicial (override possível via menu.overrides.json)
242
+ */
243
+ const menuPath = path_1.default.join(generateUiRoot, 'menu.json');
244
+ const menu = buildMenuFromRoutes(routes);
245
+ fs_1.default.writeFileSync(menuPath, JSON.stringify(menu, null, 2));
246
+ (0, logger_1.logDebug)(`Menu written: ${menuPath}`);
247
+ /**
248
+ * 4.2️⃣ Gera menu.overrides.json (não sobrescreve)
249
+ */
250
+ const menuOverridesPath = path_1.default.join(generateUiRoot, 'menu.overrides.json');
251
+ if (!fs_1.default.existsSync(menuOverridesPath)) {
252
+ fs_1.default.writeFileSync(menuOverridesPath, JSON.stringify(menu, null, 2));
253
+ (0, logger_1.logDebug)(`Menu overrides created: ${menuOverridesPath}`);
254
+ }
255
+ else {
256
+ (0, logger_1.logDebug)(`Menu overrides found: ${menuOverridesPath}`);
257
+ }
130
258
  /**
131
259
  * 5️⃣ Remove overlays órfãos (endpoint removido)
132
260
  */
@@ -135,30 +263,44 @@ async function generate(options) {
135
263
  .filter(file => file.endsWith('.screen.json'));
136
264
  for (const file of overlayFiles) {
137
265
  const opId = file.replace(/\.screen\.json$/, '');
138
- if (!operationIds.has(opId)) {
266
+ if (!operationFileNames.has(opId)) {
139
267
  fs_1.default.rmSync(path_1.default.join(overlaysDir, file));
140
268
  if (options.debug) {
141
269
  console.log(`✖ Removed overlay ${opId}`);
142
270
  }
143
271
  }
144
272
  }
145
- if (permissions.features.maxGenerations > -1) {
146
- (0, device_1.incrementFreeGeneration)();
147
- }
148
273
  console.log('✔ Routes generated');
274
+ console.log('');
275
+ console.log('🎉 Next steps');
276
+ console.log(' 1) Default full flow:');
277
+ console.log(' generate-ui generate');
278
+ console.log(' 2) Advanced (schemas only):');
279
+ console.log(' generate-ui schema');
280
+ console.log(' 3) Advanced (Angular only):');
281
+ console.log(' generate-ui angular');
282
+ console.log(' 4) Customize screens in generate-ui/overlays/');
283
+ console.log(' 5) Customize menu in generate-ui/menu.overrides.json (created once, never overwritten)');
284
+ console.log(' 6) Edit generateui-config.json to set appTitle/defaultRoute/menu.autoInject/views');
285
+ console.log('');
286
+ (0, logger_1.logTip)('Run with --dev to see detailed logs and file paths.');
287
+ }
288
+ function normalizeSubscriptionReason(reason) {
289
+ if (typeof reason !== 'string')
290
+ return '';
291
+ const trimmed = reason.trim();
292
+ return trimmed;
149
293
  }
150
294
  function resolveGenerateUiRoot(projectRoot, output) {
151
295
  if (output) {
152
- return path_1.default.resolve(process.cwd(), output);
296
+ return path_1.default.isAbsolute(output)
297
+ ? output
298
+ : path_1.default.resolve(process.cwd(), output);
153
299
  }
154
300
  const srcRoot = path_1.default.join(projectRoot, 'src');
155
301
  if (fs_1.default.existsSync(srcRoot)) {
156
302
  return path_1.default.join(srcRoot, 'generate-ui');
157
303
  }
158
- const frontendSrcRoot = path_1.default.join(projectRoot, 'frontend', 'src');
159
- if (fs_1.default.existsSync(frontendSrcRoot)) {
160
- return path_1.default.join(frontendSrcRoot, 'generate-ui');
161
- }
162
304
  return path_1.default.join(projectRoot, 'generate-ui');
163
305
  }
164
306
  function mergeParameters(pathParams, opParams) {
@@ -199,6 +341,194 @@ function buildOperationId(method, pathKey, usedOperationIds) {
199
341
  usedOperationIds.add(candidate);
200
342
  return candidate;
201
343
  }
344
+ function inferRouteGroup(op, pathKey) {
345
+ const tag = Array.isArray(op?.tags) && op.tags.length
346
+ ? String(op.tags[0]).trim()
347
+ : '';
348
+ if (tag)
349
+ return tag;
350
+ const segment = String(pathKey || '')
351
+ .split('/')
352
+ .map(part => part.trim())
353
+ .filter(Boolean)
354
+ .find(part => !part.startsWith('{') && !part.endsWith('}'));
355
+ return segment ? segment : null;
356
+ }
357
+ function recordResourceOp(map, pathKey, method, operationId, op) {
358
+ const isItemPath = /\/{[^}]+}$/.test(pathKey);
359
+ const basePath = isItemPath
360
+ ? pathKey.replace(/\/{[^}]+}$/, '')
361
+ : pathKey;
362
+ const paramMatch = isItemPath
363
+ ? pathKey.match(/\/{([^}]+)}$/)
364
+ : null;
365
+ const idParam = paramMatch ? paramMatch[1] : null;
366
+ const tag = Array.isArray(op?.tags) && op.tags.length
367
+ ? String(op.tags[0]).trim()
368
+ : undefined;
369
+ const entry = map.get(basePath) || {
370
+ basePath
371
+ };
372
+ if (tag && !entry.tag)
373
+ entry.tag = tag;
374
+ if (idParam && !entry.idParam)
375
+ entry.idParam = idParam;
376
+ const payload = {
377
+ operationId,
378
+ method,
379
+ path: pathKey,
380
+ tag
381
+ };
382
+ if (!isItemPath && method === 'get')
383
+ entry.list = payload;
384
+ if (isItemPath && method === 'get')
385
+ entry.detail = payload;
386
+ if (isItemPath && (method === 'put' || method === 'patch')) {
387
+ entry.update = payload;
388
+ }
389
+ if (isItemPath && method === 'delete')
390
+ entry.remove = payload;
391
+ map.set(basePath, entry);
392
+ }
393
+ function buildAdminSchemas(resources, usedOperationIds, screenByOpId) {
394
+ const adminSchemas = [];
395
+ for (const resource of resources.values()) {
396
+ if (!resource.list)
397
+ continue;
398
+ const entity = inferEntityName(resource.basePath);
399
+ const baseId = toPascalCase(entity);
400
+ const baseOpId = `${baseId}Admin`;
401
+ let operationId = baseOpId;
402
+ let index = 2;
403
+ while (usedOperationIds.has(operationId)) {
404
+ operationId = `${baseOpId}${index}`;
405
+ index += 1;
406
+ }
407
+ usedOperationIds.add(operationId);
408
+ const label = `${toLabel(entity)} Admin`;
409
+ const group = resource.tag ? resource.tag : inferGroup(resource.basePath);
410
+ const listSchema = screenByOpId.get(resource.list.operationId);
411
+ const columns = listSchema?.data?.table?.columns &&
412
+ Array.isArray(listSchema.data.table.columns)
413
+ ? listSchema.data.table.columns
414
+ : [];
415
+ const responseFormat = listSchema?.response?.format === 'cards' ||
416
+ listSchema?.response?.format === 'raw' ||
417
+ listSchema?.response?.format === 'table'
418
+ ? listSchema.response.format
419
+ : columns.length > 0
420
+ ? 'table'
421
+ : null;
422
+ adminSchemas.push({
423
+ meta: {
424
+ intelligent: {
425
+ kind: 'adminList',
426
+ label,
427
+ group,
428
+ listOperationId: resource.list.operationId,
429
+ detailOperationId: resource.detail?.operationId ?? null,
430
+ updateOperationId: resource.update?.operationId ?? null,
431
+ deleteOperationId: resource.remove?.operationId ?? null,
432
+ idParam: resource.idParam ?? null
433
+ }
434
+ },
435
+ entity: toLabel(entity),
436
+ description: 'Smart admin list generated from collection endpoints.',
437
+ data: {
438
+ table: {
439
+ columns
440
+ }
441
+ },
442
+ response: responseFormat
443
+ ? { format: responseFormat }
444
+ : undefined,
445
+ api: {
446
+ operationId,
447
+ method: 'get',
448
+ endpoint: resource.list.path
449
+ }
450
+ });
451
+ }
452
+ return adminSchemas;
453
+ }
454
+ function inferEntityName(pathKey) {
455
+ const segments = String(pathKey || '')
456
+ .split('/')
457
+ .filter(Boolean);
458
+ if (!segments.length)
459
+ return 'Resource';
460
+ const last = segments[segments.length - 1];
461
+ return last.replace(/[^a-zA-Z0-9]+/g, ' ');
462
+ }
463
+ function inferGroup(pathKey) {
464
+ const segment = String(pathKey || '')
465
+ .split('/')
466
+ .map(part => part.trim())
467
+ .filter(Boolean)[0];
468
+ return segment ? segment : null;
469
+ }
470
+ function buildMenuFromRoutes(routes) {
471
+ const groups = [];
472
+ const ungrouped = [];
473
+ const groupMap = new Map();
474
+ for (const route of routes) {
475
+ const item = {
476
+ id: route.operationId,
477
+ label: toLabel(route.label || route.operationId),
478
+ route: route.path
479
+ };
480
+ const rawGroup = route.group ? String(route.group) : '';
481
+ if (!rawGroup) {
482
+ ungrouped.push(item);
483
+ continue;
484
+ }
485
+ const groupId = toKebab(rawGroup);
486
+ let group = groupMap.get(groupId);
487
+ if (!group) {
488
+ group = {
489
+ id: groupId,
490
+ label: toLabel(rawGroup),
491
+ items: []
492
+ };
493
+ groupMap.set(groupId, group);
494
+ groups.push(group);
495
+ }
496
+ group.items.push(item);
497
+ }
498
+ return {
499
+ groups,
500
+ ungrouped
501
+ };
502
+ }
503
+ function toKebab(value) {
504
+ return String(value)
505
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
506
+ .replace(/[_\s]+/g, '-')
507
+ .toLowerCase();
508
+ }
509
+ function toLabel(value) {
510
+ return stripDiacritics(String(value))
511
+ .replace(/[_-]/g, ' ')
512
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
513
+ .replace(/\b\w/g, char => char.toUpperCase());
514
+ }
515
+ function toSafeFileName(value) {
516
+ return String(value)
517
+ .replace(/[\\/]/g, '-')
518
+ .replace(/\s+/g, '-');
519
+ }
520
+ function stripDiacritics(value) {
521
+ return value
522
+ .normalize('NFD')
523
+ .replace(/[\u0300-\u036f]/g, '');
524
+ }
525
+ function toPascalCase(value) {
526
+ return String(value)
527
+ .split(/[^a-zA-Z0-9]+/)
528
+ .filter(Boolean)
529
+ .map(part => part[0].toUpperCase() + part.slice(1))
530
+ .join('');
531
+ }
202
532
  function httpVerbToPrefix(verb) {
203
533
  switch (verb) {
204
534
  case 'get':