generate-ui-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.
- package/README.md +32 -0
- package/dist/commands/angular.js +39 -0
- package/dist/commands/generate.js +207 -0
- package/dist/commands/login.js +132 -0
- package/dist/generators/angular/feature.generator.js +1211 -0
- package/dist/generators/angular/routes.generator.js +45 -0
- package/dist/generators/form.generator.js +70 -0
- package/dist/generators/infer-entity.js +20 -0
- package/dist/generators/infer-submit-wrapper.js +14 -0
- package/dist/generators/screen.generator.js +134 -0
- package/dist/generators/screen.merge.js +202 -0
- package/dist/index.js +105 -0
- package/dist/license/device.js +58 -0
- package/dist/license/guard.js +9 -0
- package/dist/license/permissions.js +93 -0
- package/dist/license/token.js +46 -0
- package/dist/openapi/load-openapi.js +10 -0
- package/dist/overlay/sync-overlay.js +26 -0
- package/dist/runtime/config.js +20 -0
- package/dist/runtime/open-browser.js +26 -0
- package/dist/telemetry.js +40 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# GenerateUI CLI
|
|
2
|
+
|
|
3
|
+
Generate UI from OpenAPI locally. Free works offline with 1 generation per device; Dev unlocks unlimited generation, safe regeneration, and UI overrides.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
```bash
|
|
7
|
+
npm install -g generate-ui-cli
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
```bash
|
|
12
|
+
generate-ui generate --openapi /path/to/openapi.yaml
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Safe regeneration (Dev):
|
|
16
|
+
```bash
|
|
17
|
+
generate-ui regenerate --openapi /path/to/openapi.yaml
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Login (Dev):
|
|
21
|
+
```bash
|
|
22
|
+
generate-ui login
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Telemetry can be disabled with:
|
|
26
|
+
```bash
|
|
27
|
+
generate-ui --no-telemetry generate --openapi /path/to/openapi.yaml
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Local files
|
|
31
|
+
- `~/.generateui/device.json`
|
|
32
|
+
- `~/.generateui/token.json`
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.angular = angular;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const feature_generator_1 = require("../generators/angular/feature.generator");
|
|
10
|
+
const routes_generator_1 = require("../generators/angular/routes.generator");
|
|
11
|
+
const telemetry_1 = require("../telemetry");
|
|
12
|
+
async function angular(options) {
|
|
13
|
+
/**
|
|
14
|
+
* Onde estão os schemas
|
|
15
|
+
* Ex: frontend/src/app/assets/generate-ui
|
|
16
|
+
*/
|
|
17
|
+
const schemasRoot = path_1.default.resolve(process.cwd(), options.schemasPath);
|
|
18
|
+
const overlaysDir = path_1.default.join(schemasRoot, 'overlays');
|
|
19
|
+
if (!fs_1.default.existsSync(overlaysDir)) {
|
|
20
|
+
throw new Error(`Overlays directory not found: ${overlaysDir}`);
|
|
21
|
+
}
|
|
22
|
+
const screens = fs_1.default
|
|
23
|
+
.readdirSync(overlaysDir)
|
|
24
|
+
.filter(f => f.endsWith('.screen.json'));
|
|
25
|
+
/**
|
|
26
|
+
* Onde gerar as features Angular
|
|
27
|
+
*/
|
|
28
|
+
const featuresRoot = path_1.default.resolve(process.cwd(), options.featuresPath);
|
|
29
|
+
fs_1.default.mkdirSync(featuresRoot, { recursive: true });
|
|
30
|
+
const routes = [];
|
|
31
|
+
for (const file of screens) {
|
|
32
|
+
const schema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(overlaysDir, file), 'utf-8'));
|
|
33
|
+
const route = (0, feature_generator_1.generateFeature)(schema, featuresRoot);
|
|
34
|
+
routes.push(route);
|
|
35
|
+
}
|
|
36
|
+
(0, routes_generator_1.generateRoutes)(routes, featuresRoot);
|
|
37
|
+
await (0, telemetry_1.sendTelemetry)('generate', options.telemetryEnabled);
|
|
38
|
+
console.log(`✔ Angular features generated at ${featuresRoot}`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generate = generate;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const load_openapi_1 = require("../openapi/load-openapi");
|
|
10
|
+
const screen_generator_1 = require("../generators/screen.generator");
|
|
11
|
+
const screen_merge_1 = require("../generators/screen.merge");
|
|
12
|
+
const permissions_1 = require("../license/permissions");
|
|
13
|
+
const device_1 = require("../license/device");
|
|
14
|
+
const guard_1 = require("../license/guard");
|
|
15
|
+
const telemetry_1 = require("../telemetry");
|
|
16
|
+
async function generate(options) {
|
|
17
|
+
/**
|
|
18
|
+
* Caminho absoluto do OpenAPI (YAML)
|
|
19
|
+
* Ex: /Users/.../generateui-playground/realWorldOpenApi.yaml
|
|
20
|
+
*/
|
|
21
|
+
const openApiPath = path_1.default.resolve(process.cwd(), options.openapi);
|
|
22
|
+
/**
|
|
23
|
+
* Raiz do playground (onde está o YAML)
|
|
24
|
+
*/
|
|
25
|
+
const projectRoot = path_1.default.dirname(openApiPath);
|
|
26
|
+
/**
|
|
27
|
+
* Onde o Angular consome os arquivos
|
|
28
|
+
*/
|
|
29
|
+
const generateUiRoot = path_1.default.join(projectRoot, 'frontend', 'src', 'app', 'assets', 'generate-ui');
|
|
30
|
+
const generatedDir = path_1.default.join(generateUiRoot, 'generated');
|
|
31
|
+
const overlaysDir = path_1.default.join(generateUiRoot, 'overlays');
|
|
32
|
+
fs_1.default.mkdirSync(generatedDir, { recursive: true });
|
|
33
|
+
fs_1.default.mkdirSync(overlaysDir, { recursive: true });
|
|
34
|
+
/**
|
|
35
|
+
* Lista de rotas geradas automaticamente
|
|
36
|
+
*/
|
|
37
|
+
const routes = [];
|
|
38
|
+
const usedOperationIds = new Set();
|
|
39
|
+
const permissions = await (0, permissions_1.getPermissions)();
|
|
40
|
+
const device = (0, device_1.loadDeviceIdentity)();
|
|
41
|
+
await (0, telemetry_1.sendTelemetry)(options.telemetryCommand, options.telemetryEnabled);
|
|
42
|
+
if (options.requireSafeRegeneration) {
|
|
43
|
+
(0, guard_1.requireFeature)(permissions.features, 'safeRegeneration', 'Regeneration requires safe regeneration.');
|
|
44
|
+
}
|
|
45
|
+
if (permissions.features.maxGenerations > -1 &&
|
|
46
|
+
device.freeGenerationsUsed >= permissions.features.maxGenerations) {
|
|
47
|
+
throw new Error('🔒 Você já utilizou sua geração gratuita.\n' +
|
|
48
|
+
'O plano Dev libera gerações ilimitadas, regeneração segura e UI inteligente.\n' +
|
|
49
|
+
'👉 Execute `generate-ui login` para continuar.');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Parse do OpenAPI (já com $refs resolvidos)
|
|
53
|
+
*/
|
|
54
|
+
const api = await (0, load_openapi_1.loadOpenApi)(openApiPath);
|
|
55
|
+
const paths = api.paths ?? {};
|
|
56
|
+
/**
|
|
57
|
+
* Itera por todos os endpoints
|
|
58
|
+
*/
|
|
59
|
+
const operationIds = new Set();
|
|
60
|
+
for (const [pathKey, pathItem] of Object.entries(paths)) {
|
|
61
|
+
for (const [method, rawOp] of Object.entries(pathItem)) {
|
|
62
|
+
const op = rawOp;
|
|
63
|
+
if (!op)
|
|
64
|
+
continue;
|
|
65
|
+
const operationId = op.operationId ||
|
|
66
|
+
buildOperationId(method.toLowerCase(), pathKey, usedOperationIds);
|
|
67
|
+
operationIds.add(operationId);
|
|
68
|
+
const endpoint = {
|
|
69
|
+
operationId,
|
|
70
|
+
path: pathKey,
|
|
71
|
+
method: method.toLowerCase(),
|
|
72
|
+
parameters: mergeParameters(pathItem?.parameters, op?.parameters),
|
|
73
|
+
...op
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Gera o ScreenSchema completo
|
|
77
|
+
*/
|
|
78
|
+
const screenSchema = (0, screen_generator_1.generateScreen)(endpoint, api);
|
|
79
|
+
const fileName = `${operationId}.screen.json`;
|
|
80
|
+
/**
|
|
81
|
+
* 1️⃣ generated → SEMPRE sobrescrito (base técnica)
|
|
82
|
+
*/
|
|
83
|
+
const generatedPath = path_1.default.join(generatedDir, fileName);
|
|
84
|
+
const previousGenerated = fs_1.default.existsSync(generatedPath)
|
|
85
|
+
? JSON.parse(fs_1.default.readFileSync(generatedPath, 'utf-8'))
|
|
86
|
+
: null;
|
|
87
|
+
fs_1.default.writeFileSync(generatedPath, JSON.stringify(screenSchema, null, 2));
|
|
88
|
+
/**
|
|
89
|
+
* 2️⃣ overlays → merge semântico (preserva decisões do usuário)
|
|
90
|
+
*/
|
|
91
|
+
const overlayPath = path_1.default.join(overlaysDir, fileName);
|
|
92
|
+
const canOverride = permissions.features.uiOverrides;
|
|
93
|
+
const canRegenerateSafely = permissions.features.safeRegeneration;
|
|
94
|
+
if (canOverride && canRegenerateSafely) {
|
|
95
|
+
const overlay = fs_1.default.existsSync(overlayPath)
|
|
96
|
+
? JSON.parse(fs_1.default.readFileSync(overlayPath, 'utf-8'))
|
|
97
|
+
: null;
|
|
98
|
+
const merged = (0, screen_merge_1.mergeScreen)(screenSchema, overlay, previousGenerated, {
|
|
99
|
+
openapiVersion: api?.info?.version || 'unknown',
|
|
100
|
+
debug: options.debug
|
|
101
|
+
});
|
|
102
|
+
fs_1.default.writeFileSync(overlayPath, JSON.stringify(merged.screen, null, 2));
|
|
103
|
+
if (options.debug && merged.debug.length) {
|
|
104
|
+
console.log(`ℹ Merge ${operationId}`);
|
|
105
|
+
for (const line of merged.debug) {
|
|
106
|
+
console.log(` - ${line}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
fs_1.default.writeFileSync(overlayPath, JSON.stringify(screenSchema, null, 2));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 3️⃣ rota automática
|
|
115
|
+
* URL = operationId (MVP)
|
|
116
|
+
*/
|
|
117
|
+
routes.push({
|
|
118
|
+
path: operationId,
|
|
119
|
+
operationId
|
|
120
|
+
});
|
|
121
|
+
console.log(`✔ Generated ${operationId}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 4️⃣ Gera arquivo de rotas
|
|
126
|
+
*/
|
|
127
|
+
const routesPath = path_1.default.join(generateUiRoot, 'routes.json');
|
|
128
|
+
fs_1.default.writeFileSync(routesPath, JSON.stringify(routes, null, 2));
|
|
129
|
+
/**
|
|
130
|
+
* 5️⃣ Remove overlays órfãos (endpoint removido)
|
|
131
|
+
*/
|
|
132
|
+
const overlayFiles = fs_1.default
|
|
133
|
+
.readdirSync(overlaysDir)
|
|
134
|
+
.filter(file => file.endsWith('.screen.json'));
|
|
135
|
+
for (const file of overlayFiles) {
|
|
136
|
+
const opId = file.replace(/\.screen\.json$/, '');
|
|
137
|
+
if (!operationIds.has(opId)) {
|
|
138
|
+
fs_1.default.rmSync(path_1.default.join(overlaysDir, file));
|
|
139
|
+
if (options.debug) {
|
|
140
|
+
console.log(`✖ Removed overlay ${opId}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (permissions.features.maxGenerations > -1) {
|
|
145
|
+
(0, device_1.incrementFreeGeneration)();
|
|
146
|
+
}
|
|
147
|
+
console.log('✔ Routes generated');
|
|
148
|
+
}
|
|
149
|
+
function mergeParameters(pathParams, opParams) {
|
|
150
|
+
const all = [...(pathParams ?? []), ...(opParams ?? [])];
|
|
151
|
+
const seen = new Set();
|
|
152
|
+
const merged = [];
|
|
153
|
+
for (const param of all) {
|
|
154
|
+
const key = `${param?.in ?? ''}:${param?.name ?? ''}`;
|
|
155
|
+
if (seen.has(key))
|
|
156
|
+
continue;
|
|
157
|
+
seen.add(key);
|
|
158
|
+
merged.push(param);
|
|
159
|
+
}
|
|
160
|
+
return merged;
|
|
161
|
+
}
|
|
162
|
+
function buildOperationId(method, pathKey, usedOperationIds) {
|
|
163
|
+
const verb = method.toLowerCase();
|
|
164
|
+
const prefix = httpVerbToPrefix(verb);
|
|
165
|
+
const parts = pathKey
|
|
166
|
+
.split('/')
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.map(segment => {
|
|
169
|
+
if (segment.startsWith('{') && segment.endsWith('}')) {
|
|
170
|
+
const name = segment.slice(1, -1);
|
|
171
|
+
return `By${capitalize(name)}`;
|
|
172
|
+
}
|
|
173
|
+
return capitalize(segment);
|
|
174
|
+
});
|
|
175
|
+
let base = `${prefix}${parts.join('')}`;
|
|
176
|
+
if (!base)
|
|
177
|
+
base = `${prefix}Endpoint`;
|
|
178
|
+
let candidate = base;
|
|
179
|
+
let index = 2;
|
|
180
|
+
while (usedOperationIds.has(candidate)) {
|
|
181
|
+
candidate = `${base}${index}`;
|
|
182
|
+
index += 1;
|
|
183
|
+
}
|
|
184
|
+
usedOperationIds.add(candidate);
|
|
185
|
+
return candidate;
|
|
186
|
+
}
|
|
187
|
+
function httpVerbToPrefix(verb) {
|
|
188
|
+
switch (verb) {
|
|
189
|
+
case 'get':
|
|
190
|
+
return 'Get';
|
|
191
|
+
case 'post':
|
|
192
|
+
return 'Create';
|
|
193
|
+
case 'put':
|
|
194
|
+
return 'Update';
|
|
195
|
+
case 'patch':
|
|
196
|
+
return 'Patch';
|
|
197
|
+
case 'delete':
|
|
198
|
+
return 'Delete';
|
|
199
|
+
default:
|
|
200
|
+
return 'Call';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function capitalize(value) {
|
|
204
|
+
if (!value)
|
|
205
|
+
return value;
|
|
206
|
+
return value[0].toUpperCase() + value.slice(1);
|
|
207
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.login = login;
|
|
7
|
+
const http_1 = __importDefault(require("http"));
|
|
8
|
+
const url_1 = require("url");
|
|
9
|
+
const config_1 = require("../runtime/config");
|
|
10
|
+
const open_browser_1 = require("../runtime/open-browser");
|
|
11
|
+
const token_1 = require("../license/token");
|
|
12
|
+
const permissions_1 = require("../license/permissions");
|
|
13
|
+
const telemetry_1 = require("../telemetry");
|
|
14
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
15
|
+
async function login(options) {
|
|
16
|
+
const token = await waitForLogin();
|
|
17
|
+
(0, token_1.saveToken)(token);
|
|
18
|
+
try {
|
|
19
|
+
await (0, permissions_1.fetchPermissions)();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Cached permissions will be refreshed on next online command.
|
|
23
|
+
}
|
|
24
|
+
await (0, telemetry_1.sendTelemetry)('login', options.telemetryEnabled);
|
|
25
|
+
console.log('✔ Login completo');
|
|
26
|
+
}
|
|
27
|
+
async function waitForLogin() {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const server = http_1.default.createServer((req, res) => {
|
|
30
|
+
const requestUrl = req.url || '/';
|
|
31
|
+
if (!requestUrl.startsWith('/callback')) {
|
|
32
|
+
res.writeHead(404);
|
|
33
|
+
res.end('Not Found');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const url = new url_1.URL(requestUrl, 'http://localhost');
|
|
37
|
+
const accessToken = url.searchParams.get('access_token');
|
|
38
|
+
const expiresAtParam = url.searchParams.get('expires_at');
|
|
39
|
+
if (!accessToken) {
|
|
40
|
+
res.writeHead(400);
|
|
41
|
+
res.end('Missing access token');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const expiresAt = expiresAtParam ||
|
|
45
|
+
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
46
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
47
|
+
res.end(`<!doctype html>
|
|
48
|
+
<html lang="en">
|
|
49
|
+
<head>
|
|
50
|
+
<meta charset="utf-8" />
|
|
51
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
52
|
+
<title>GenerateUI</title>
|
|
53
|
+
<style>
|
|
54
|
+
:root {
|
|
55
|
+
--bg: #f3e8ff;
|
|
56
|
+
--card: #ffffff;
|
|
57
|
+
--text: #2a1b3d;
|
|
58
|
+
--muted: #6b5b7a;
|
|
59
|
+
--primary: #7c3aed;
|
|
60
|
+
--glow: rgba(124, 58, 237, 0.22);
|
|
61
|
+
}
|
|
62
|
+
* {
|
|
63
|
+
box-sizing: border-box;
|
|
64
|
+
font-family: "IBM Plex Serif", "Georgia", serif;
|
|
65
|
+
}
|
|
66
|
+
body {
|
|
67
|
+
margin: 0;
|
|
68
|
+
min-height: 100vh;
|
|
69
|
+
display: grid;
|
|
70
|
+
place-items: center;
|
|
71
|
+
background: radial-gradient(circle at top, #f5ebff, #e9d5ff);
|
|
72
|
+
color: var(--text);
|
|
73
|
+
}
|
|
74
|
+
main {
|
|
75
|
+
background: var(--card);
|
|
76
|
+
padding: 52px 48px;
|
|
77
|
+
border-radius: 24px;
|
|
78
|
+
box-shadow: 0 24px 70px var(--glow);
|
|
79
|
+
width: min(460px, 92vw);
|
|
80
|
+
text-align: center;
|
|
81
|
+
}
|
|
82
|
+
h1 {
|
|
83
|
+
margin: 0 0 12px;
|
|
84
|
+
font-size: 30px;
|
|
85
|
+
}
|
|
86
|
+
p {
|
|
87
|
+
margin: 0 0 24px;
|
|
88
|
+
color: var(--muted);
|
|
89
|
+
font-size: 16px;
|
|
90
|
+
}
|
|
91
|
+
.pill {
|
|
92
|
+
display: inline-block;
|
|
93
|
+
background: #f5e9ff;
|
|
94
|
+
color: var(--primary);
|
|
95
|
+
padding: 8px 14px;
|
|
96
|
+
border-radius: 999px;
|
|
97
|
+
font-weight: 600;
|
|
98
|
+
letter-spacing: 0.2px;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
101
|
+
</head>
|
|
102
|
+
<body>
|
|
103
|
+
<main>
|
|
104
|
+
<h1>Let's Generate UI</h1>
|
|
105
|
+
<p>Login completed successfully.</p>
|
|
106
|
+
<span class="pill">You can close this window</span>
|
|
107
|
+
</main>
|
|
108
|
+
</body>
|
|
109
|
+
</html>`);
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
server.close();
|
|
112
|
+
resolve({ accessToken, expiresAt });
|
|
113
|
+
});
|
|
114
|
+
const timeout = setTimeout(() => {
|
|
115
|
+
server.close();
|
|
116
|
+
reject(new Error('Login timed out'));
|
|
117
|
+
}, LOGIN_TIMEOUT_MS);
|
|
118
|
+
server.listen(0, () => {
|
|
119
|
+
const address = server.address();
|
|
120
|
+
if (!address || typeof address === 'string') {
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
reject(new Error('Failed to start login server'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const redirectUri = `http://localhost:${address.port}/callback`;
|
|
126
|
+
const url = new url_1.URL((0, config_1.getWebAuthUrl)());
|
|
127
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
128
|
+
url.searchParams.set('api_base', (0, config_1.getApiBaseUrl)());
|
|
129
|
+
(0, open_browser_1.openBrowser)(url.toString());
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|