generate-ui-cli 1.0.0 → 2.1.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
@@ -1,32 +1,310 @@
1
1
  # GenerateUI CLI
2
2
 
3
- Generate UI from OpenAPI locally. Free works offline with 1 generation per device; Dev unlocks unlimited generation, safe regeneration, and UI overrides.
3
+ Generate CRUD screens (List, Create/Edit, Delete), typed API services, and routes from your OpenAPI spec with real Angular code you can own and evolve.
4
4
 
5
- ## Install
5
+ Goal: stop rewriting repetitive CRUD and start with a functional, scalable UI foundation.
6
+
7
+ ## What GenerateUI Does
8
+
9
+ Given an `openapi.yaml` (or `.json`), GenerateUI can generate:
10
+
11
+ - `screens.json` (detected screens/endpoints)
12
+ - one folder per feature/screen
13
+ - typed API services and DTOs
14
+ - plug-and-play routes
15
+ - basic CRUD UI (list + form + delete confirmation)
16
+ - UI states (loading / empty / error)
17
+
18
+ GenerateUI is code generation, not runtime rendering.
19
+
20
+ ## Before You Start (Quick Checklist)
21
+
22
+ You will need:
23
+
24
+ - Node.js (LTS recommended)
25
+ - A valid OpenAPI v3.x file
26
+ - An Angular project (for Angular generation) > v.15
27
+ - Optional: a design system (Material, PrimeNG, internal DS)
28
+
29
+ Important:
30
+ - Incomplete OpenAPI specs (missing schemas, responses, or types) may limit what can be generated.
31
+ - Some public APIs require query params (e.g. `fields=...`). Make sure your API calls actually work.
32
+
33
+ ## Installation
34
+
35
+ ### Global install
6
36
  ```bash
7
37
  npm install -g generate-ui-cli
8
38
  ```
9
39
 
10
- ## Usage
40
+ ### Local install
11
41
  ```bash
12
- generate-ui generate --openapi /path/to/openapi.yaml
42
+ npm install -D generate-ui-cli
43
+ ```
44
+
45
+ Then run:
46
+ ```bash
47
+ npx generate-ui --help
48
+ ```
49
+
50
+ ## Recommended Workflow
51
+
52
+ GenerateUI works in two main steps:
53
+
54
+ 1. Read the OpenAPI and generate `screens.json`
55
+ 2. Generate Angular code from `screens.json`
56
+
57
+ ## 1) Generate `screens.json`
58
+
59
+ ```bash
60
+ generate-ui generate --openapi openapiWeather.yaml
61
+ ```
62
+
63
+ What happens after this command:
64
+
65
+ - GenerateUI reads your OpenAPI and detects endpoints.
66
+ - It identifies CRUD-like operations (list, get by id, create, update, delete).
67
+ - It maps request/response schemas.
68
+ - If your project has a `src/` folder, GenerateUI creates `src/generate-ui/`.
69
+ - Otherwise it creates `generate-ui/` next to your OpenAPI file.
70
+ - Inside it you get `generated/`, `overlays/`, `screens.json`, `routes.json`, and `routes.gen.ts`.
71
+
72
+ What you should review now:
73
+
74
+ - Are all expected screens present?
75
+ - Are screen and route names correct?
76
+ - Are required query params represented?
77
+ - Do the detected fields match your API schemas?
78
+
79
+ Tip: this is the best moment to adjust naming and structure before generating code.
80
+
81
+ ## 2) Generate Angular code from `screens.json`
82
+
83
+ ```bash
84
+ generate-ui angular
13
85
  ```
14
86
 
15
- Safe regeneration (Dev):
87
+ What happens after this command:
88
+
89
+ - For each screen defined in `screens.json`, GenerateUI creates:
90
+ - a feature folder
91
+ - list and form components (create/edit)
92
+ - a typed API service
93
+ - DTO/types files
94
+ - route definitions
95
+
96
+ What you should review now:
97
+
98
+ - Are files generated in the correct location?
99
+ - Does the project compile?
100
+ - Are routes correctly generated and importable?
101
+ - Does the basic UI work end-to-end?
102
+
103
+ Note:
104
+ If your project uses custom routing, standalone components, or advanced layouts, you may need to adjust how routes are plugged in.
105
+
106
+ Defaults:
107
+ - `--schemas` defaults to the last generated path (stored in `~/.generateui/config.json`), otherwise `./src/generate-ui` (or `./frontend/src/generate-ui` / `./generate-ui`)
108
+ - `--features` defaults to `./src/app/features` when it exists, otherwise `./frontend/src/app/features` or `./features`
109
+
110
+ Optional paths:
111
+
16
112
  ```bash
17
- generate-ui regenerate --openapi /path/to/openapi.yaml
113
+ generate-ui angular \
114
+ --schemas /path/to/generate-ui \
115
+ --features /path/to/angular/features
18
116
  ```
19
117
 
20
- Login (Dev):
118
+ Custom output folder for `generate`:
119
+
120
+ ```bash
121
+ generate-ui generate --openapi youropenapi.yaml --output /path/to/generate-ui
122
+ ```
123
+
124
+ ## Login (Dev plan)
125
+
21
126
  ```bash
22
127
  generate-ui login
23
128
  ```
24
129
 
25
- Telemetry can be disabled with:
130
+ What happens after this command:
131
+
132
+ - You authenticate your device to unlock Dev features.
133
+ - Dev features include safe regeneration, UI overrides, and unlimited generations.
134
+
135
+ ## Telemetry
136
+
137
+ GenerateUI collects anonymous usage data such as CLI version, OS, and executed commands to improve the product.
138
+ No source code or OpenAPI content is ever sent.
139
+ Telemetry can be disabled by setting `telemetry=false` in `~/.generateui/config.json` or by running with `--no-telemetry`.
140
+
141
+ ## Plugging Routes into Your App
142
+
143
+ GenerateUI usually creates route files such as:
144
+
145
+ - `src/generate-ui/routes.gen.ts` or `frontend/src/generate-ui/routes.gen.ts` or `generate-ui/routes.gen.ts`
146
+
147
+ Example (Angular Router):
148
+
149
+ ```ts
150
+ import { generatedRoutes } from '../generate-ui/routes.gen';
151
+
152
+ export const routes = [
153
+ // ...your existing routes
154
+ ...generatedRoutes
155
+ ];
156
+ ```
157
+
158
+ Things to pay attention to:
159
+
160
+ - route prefixes (`/admin`, `/app`, etc.)
161
+ - authentication guards
162
+ - layout components (`<router-outlet>` placement)
163
+
164
+ ## Angular >= 15 (standalone) setup
165
+
166
+ Step-by-step:
167
+
168
+ 1) Generate files:
169
+
170
+ ```bash
171
+ generate-ui generate --openapi /path/to/openapi.yaml
172
+ generate-ui angular
173
+ ```
174
+
175
+ 2) Import generated routes in `src/app/app.routes.ts`:
176
+
177
+ ```ts
178
+ import { generatedRoutes } from '../generate-ui/routes.gen'
179
+
180
+ export const routes: Routes = [
181
+ // your existing routes
182
+ ...generatedRoutes
183
+ ]
184
+ ```
185
+
186
+ 3) Ensure `provideRouter` is used in `src/main.ts`:
187
+
188
+ ```ts
189
+ import { provideRouter } from '@angular/router'
190
+ import { routes } from './app/app.routes'
191
+
192
+ bootstrapApplication(AppComponent, {
193
+ providers: [provideRouter(routes)]
194
+ })
195
+ ```
196
+
197
+ 4) Check `@angular/router` is installed:
198
+
199
+ ```bash
200
+ npm ls @angular/router
201
+ ```
202
+
203
+ ## Example Generated Structure
204
+
205
+ ```txt
206
+ src/generate-ui/ or frontend/src/generate-ui/ or generate-ui/
207
+ generated/
208
+ overlays/
209
+ routes.json
210
+ routes.gen.ts
211
+ screens.json
212
+ frontend/src/app/features/
213
+ users/
214
+ users.component.ts
215
+ users.service.gen.ts
216
+ users.gen.ts
217
+ orders/
218
+ orders.component.ts
219
+ orders.service.gen.ts
220
+ orders.gen.ts
221
+ ```
222
+
223
+ ## After Generation: How to Customize Safely
224
+
225
+ GenerateUI gives you a working baseline. From here, you typically:
226
+
227
+ - Customize UI (design system components, masks, validators)
228
+ - Add business logic (conditional fields, permissions)
229
+ - Improve UX (pagination, filtering, empty/error states)
230
+
231
+ Rule of thumb: the generated code is yours — generate once, then evolve freely.
232
+
233
+ ## Overrides and Regeneration Behavior
234
+
235
+ You can edit files inside `overlays/` to customize labels, placeholders, hints, and other details. When your API changes and you regenerate, GenerateUI updates what is safe to change from the OpenAPI, but preserves what you defined in `overlays/` to avoid breaking your flow.
236
+
237
+ Even after the Angular TypeScript files are generated, changes you make in `overlays/` will be mirrored the next time you regenerate.
238
+
239
+ ## Common Issues and Fixes
240
+
241
+ ### "required option '-o, --openapi <path>' not specified"
242
+
243
+ You ran the command without passing the OpenAPI file.
244
+
245
+ Fix:
26
246
  ```bash
27
- generate-ui --no-telemetry generate --openapi /path/to/openapi.yaml
247
+ generate-ui generate --openapi /path/to/openapi.yaml
28
248
  ```
29
249
 
30
- ## Local files
250
+ ### "An endpoint exists but no screen was generated"
251
+
252
+ This may happen if:
253
+
254
+ - `operationId` is missing
255
+ - request/response schemas are empty
256
+ - required response codes (`200`, `201`) are missing
257
+
258
+ Recommendation:
259
+
260
+ - always define `operationId`
261
+ - include schemas in responses
262
+
263
+ ### "Routes were generated but navigation does not work"
264
+
265
+ Usually a routing integration issue.
266
+
267
+ Check:
268
+
269
+ - if `GENERATED_ROUTES` is imported/spread
270
+ - if route prefixes match your menu
271
+ - if there is a `<router-outlet>` in your layout
272
+
273
+ ## Team Workflow Recommendation
274
+
275
+ 1. Update OpenAPI
276
+ 2. Generate `screens.json`
277
+ 3. Review `screens.json`
278
+ 4. Generate Angular code
279
+ 5. Customize UI and business rules
280
+ 6. Commit
281
+
282
+ ## Tips for Better Results
283
+
284
+ - Use consistent `operationId`s (`users_list`, `users_create`, etc.)
285
+ - Define complete schemas (types, required, enums)
286
+ - Standardize responses (`200`, `201`, `204`)
287
+ - Document important query params (pagination, filters)
288
+ - If your API requires `fields=...`, reflect it in `screens.json`
289
+
290
+ ## Roadmap (Example)
291
+
292
+ - [ ] Layout presets (minimal / enterprise / dashboard)
293
+ - [ ] Design system adapters (Material / PrimeNG / custom)
294
+ - [ ] Filters and real pagination
295
+ - [ ] UI schema overrides (visual control without touching OpenAPI)
296
+ - [ ] React support
297
+
298
+ ## Contributing
299
+
300
+ Issues and PRs are welcome.
301
+ If you use GenerateUI in a company or real project, let us know — it helps guide the roadmap.
302
+
303
+ ## License
304
+
305
+ MIT
306
+
307
+ ## Local Files
308
+
31
309
  - `~/.generateui/device.json`
32
310
  - `~/.generateui/token.json`
@@ -9,12 +9,15 @@ 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
11
  const telemetry_1 = require("../telemetry");
12
+ const user_config_1 = require("../runtime/user-config");
12
13
  async function angular(options) {
14
+ void (0, telemetry_1.trackCommand)('angular', options.telemetryEnabled);
15
+ const featuresRoot = resolveFeaturesRoot(options.featuresPath);
16
+ const schemasRoot = resolveSchemasRoot(options.schemasPath, featuresRoot);
13
17
  /**
14
18
  * Onde estão os schemas
15
- * Ex: frontend/src/app/assets/generate-ui
19
+ * Ex: generate-ui
16
20
  */
17
- const schemasRoot = path_1.default.resolve(process.cwd(), options.schemasPath);
18
21
  const overlaysDir = path_1.default.join(schemasRoot, 'overlays');
19
22
  if (!fs_1.default.existsSync(overlaysDir)) {
20
23
  throw new Error(`Overlays directory not found: ${overlaysDir}`);
@@ -25,15 +28,60 @@ async function angular(options) {
25
28
  /**
26
29
  * Onde gerar as features Angular
27
30
  */
28
- const featuresRoot = path_1.default.resolve(process.cwd(), options.featuresPath);
29
31
  fs_1.default.mkdirSync(featuresRoot, { recursive: true });
30
32
  const routes = [];
31
33
  for (const file of screens) {
32
34
  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);
35
+ const route = (0, feature_generator_1.generateFeature)(schema, featuresRoot, schemasRoot);
34
36
  routes.push(route);
35
37
  }
36
- (0, routes_generator_1.generateRoutes)(routes, featuresRoot);
37
- await (0, telemetry_1.sendTelemetry)('generate', options.telemetryEnabled);
38
+ (0, routes_generator_1.generateRoutes)(routes, featuresRoot, schemasRoot);
38
39
  console.log(`✔ Angular features generated at ${featuresRoot}`);
39
40
  }
41
+ function resolveSchemasRoot(value, featuresRoot) {
42
+ if (value) {
43
+ return path_1.default.resolve(process.cwd(), value);
44
+ }
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
+ const inferred = inferSchemasRootFromFeatures(featuresRoot);
53
+ if (inferred)
54
+ return inferred;
55
+ return resolveDefaultSchemasRoot();
56
+ }
57
+ function resolveFeaturesRoot(value) {
58
+ if (value) {
59
+ return path_1.default.resolve(process.cwd(), value);
60
+ }
61
+ const defaultApp = path_1.default.resolve(process.cwd(), 'src', 'app', 'features');
62
+ if (fs_1.default.existsSync(defaultApp)) {
63
+ return defaultApp;
64
+ }
65
+ const defaultFrontend = path_1.default.resolve(process.cwd(), 'frontend', 'src', 'app', 'features');
66
+ if (fs_1.default.existsSync(defaultFrontend)) {
67
+ return defaultFrontend;
68
+ }
69
+ return path_1.default.resolve(process.cwd(), 'features');
70
+ }
71
+ function inferSchemasRootFromFeatures(featuresRoot) {
72
+ const candidate = path_1.default.resolve(featuresRoot, '../../..', 'generate-ui');
73
+ if (fs_1.default.existsSync(path_1.default.join(candidate, 'overlays'))) {
74
+ return candidate;
75
+ }
76
+ return null;
77
+ }
78
+ function resolveDefaultSchemasRoot() {
79
+ const cwd = process.cwd();
80
+ if (fs_1.default.existsSync(path_1.default.join(cwd, 'src'))) {
81
+ return path_1.default.join(cwd, 'src', 'generate-ui');
82
+ }
83
+ if (fs_1.default.existsSync(path_1.default.join(cwd, 'frontend', 'src'))) {
84
+ return path_1.default.join(cwd, 'frontend', 'src', 'generate-ui');
85
+ }
86
+ return path_1.default.join(cwd, 'generate-ui');
87
+ }
@@ -11,9 +11,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
13
  const device_1 = require("../license/device");
14
- const guard_1 = require("../license/guard");
15
14
  const telemetry_1 = require("../telemetry");
15
+ const user_config_1 = require("../runtime/user-config");
16
16
  async function generate(options) {
17
+ void (0, telemetry_1.trackCommand)('generate', options.telemetryEnabled);
17
18
  /**
18
19
  * Caminho absoluto do OpenAPI (YAML)
19
20
  * Ex: /Users/.../generateui-playground/realWorldOpenApi.yaml
@@ -26,9 +27,13 @@ async function generate(options) {
26
27
  /**
27
28
  * Onde o Angular consome os arquivos
28
29
  */
29
- const generateUiRoot = path_1.default.join(projectRoot, 'frontend', 'src', 'app', 'assets', 'generate-ui');
30
+ const generateUiRoot = resolveGenerateUiRoot(projectRoot, options.output);
30
31
  const generatedDir = path_1.default.join(generateUiRoot, 'generated');
31
32
  const overlaysDir = path_1.default.join(generateUiRoot, 'overlays');
33
+ (0, user_config_1.updateUserConfig)(config => ({
34
+ ...config,
35
+ lastSchemasPath: generateUiRoot
36
+ }));
32
37
  fs_1.default.mkdirSync(generatedDir, { recursive: true });
33
38
  fs_1.default.mkdirSync(overlaysDir, { recursive: true });
34
39
  /**
@@ -38,10 +43,6 @@ async function generate(options) {
38
43
  const usedOperationIds = new Set();
39
44
  const permissions = await (0, permissions_1.getPermissions)();
40
45
  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
46
  if (permissions.features.maxGenerations > -1 &&
46
47
  device.freeGenerationsUsed >= permissions.features.maxGenerations) {
47
48
  throw new Error('🔒 Você já utilizou sua geração gratuita.\n' +
@@ -146,6 +147,20 @@ async function generate(options) {
146
147
  }
147
148
  console.log('✔ Routes generated');
148
149
  }
150
+ function resolveGenerateUiRoot(projectRoot, output) {
151
+ if (output) {
152
+ return path_1.default.resolve(process.cwd(), output);
153
+ }
154
+ const srcRoot = path_1.default.join(projectRoot, 'src');
155
+ if (fs_1.default.existsSync(srcRoot)) {
156
+ return path_1.default.join(srcRoot, 'generate-ui');
157
+ }
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
+ return path_1.default.join(projectRoot, 'generate-ui');
163
+ }
149
164
  function mergeParameters(pathParams, opParams) {
150
165
  const all = [...(pathParams ?? []), ...(opParams ?? [])];
151
166
  const seen = new Set();
@@ -6,6 +6,7 @@ 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
10
  const config_1 = require("../runtime/config");
10
11
  const open_browser_1 = require("../runtime/open-browser");
11
12
  const token_1 = require("../license/token");
@@ -13,6 +14,7 @@ const permissions_1 = require("../license/permissions");
13
14
  const telemetry_1 = require("../telemetry");
14
15
  const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
15
16
  async function login(options) {
17
+ void (0, telemetry_1.trackCommand)('login', options.telemetryEnabled);
16
18
  const token = await waitForLogin();
17
19
  (0, token_1.saveToken)(token);
18
20
  try {
@@ -21,9 +23,26 @@ async function login(options) {
21
23
  catch {
22
24
  // Cached permissions will be refreshed on next online command.
23
25
  }
24
- await (0, telemetry_1.sendTelemetry)('login', options.telemetryEnabled);
26
+ const email = await promptEmail();
27
+ await (0, telemetry_1.trackLogin)(email, options.telemetryEnabled);
25
28
  console.log('✔ Login completo');
26
29
  }
30
+ async function promptEmail() {
31
+ if (!process.stdin.isTTY)
32
+ return null;
33
+ const rl = (0, promises_1.createInterface)({
34
+ input: process.stdin,
35
+ output: process.stdout
36
+ });
37
+ try {
38
+ const value = await rl.question('Email (optional): ');
39
+ const trimmed = value.trim();
40
+ return trimmed.length ? trimmed : null;
41
+ }
42
+ finally {
43
+ rl.close();
44
+ }
45
+ }
27
46
  async function waitForLogin() {
28
47
  return new Promise((resolve, reject) => {
29
48
  const server = http_1.default.createServer((req, res) => {
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.generateFeature = generateFeature;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
- function generateFeature(schema, root) {
9
+ function generateFeature(schema, root, schemasRoot) {
10
10
  const rawName = schema.api.operationId;
11
11
  const name = toPascalCase(rawName);
12
12
  const folder = toFolderName(name);
@@ -47,6 +47,7 @@ function generateFeature(schema, root) {
47
47
  ? String(schema.entity).trim()
48
48
  : rawName;
49
49
  const subtitle = `${method.toUpperCase()} ${endpoint}`;
50
+ const schemaImportPath = buildSchemaImportPath(featureDir, schemasRoot, rawName);
50
51
  /**
51
52
  * 1️⃣ Component (sempre sobrescreve)
52
53
  */
@@ -60,7 +61,7 @@ import { UiFieldComponent } from '../../ui/ui-field/ui-field.component'
60
61
  import { UiButtonComponent } from '../../ui/ui-button/ui-button.component'
61
62
  import { ${name}Service } from './${fileBase}.service.gen'
62
63
  import { ${name}Gen } from './${fileBase}.gen'
63
- import screenSchema from '../../assets/generate-ui/overlays/${rawName}.screen.json'
64
+ import screenSchema from '${schemaImportPath}'
64
65
 
65
66
  @Component({
66
67
  selector: 'app-${toKebab(name)}',
@@ -1209,3 +1210,15 @@ function toPascalCase(value) {
1209
1210
  .map(part => part[0].toUpperCase() + part.slice(1))
1210
1211
  .join('');
1211
1212
  }
1213
+ function buildSchemaImportPath(featureDir, schemasRoot, rawName) {
1214
+ const schemaFile = path_1.default.join(schemasRoot, 'overlays', `${rawName}.screen.json`);
1215
+ let relativePath = path_1.default.relative(featureDir, schemaFile);
1216
+ relativePath = toPosixPath(relativePath);
1217
+ if (!relativePath.startsWith('.')) {
1218
+ relativePath = `./${relativePath}`;
1219
+ }
1220
+ return relativePath;
1221
+ }
1222
+ function toPosixPath(value) {
1223
+ return value.split(path_1.default.sep).join(path_1.default.posix.sep);
1224
+ }
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.generateRoutes = generateRoutes;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
- function generateRoutes(routes, featuresRoot) {
10
- const appRoot = path_1.default.resolve(featuresRoot, '..');
11
- const out = path_1.default.join(appRoot, 'generated', 'routes.gen.ts');
9
+ function generateRoutes(routes, featuresRoot, schemasRoot) {
10
+ const out = path_1.default.join(schemasRoot, 'routes.gen.ts');
11
+ const featuresImportBase = buildRelativeImportBase(schemasRoot, featuresRoot);
12
12
  fs_1.default.mkdirSync(path_1.default.dirname(out), { recursive: true });
13
13
  const content = `
14
14
  import { Routes } from '@angular/router'
@@ -16,10 +16,11 @@ import { Routes } from '@angular/router'
16
16
  export const generatedRoutes: Routes = [
17
17
  ${routes
18
18
  .flatMap(r => {
19
+ const baseImport = ensureRelativeImport(toPosixPath(path_1.default.join(featuresImportBase, r.folder, `${r.fileBase}.component`)));
19
20
  const base = ` {
20
21
  path: '${r.path}',
21
22
  loadComponent: () =>
22
- import('../features/${r.folder}/${r.fileBase}.component')
23
+ import('${baseImport}')
23
24
  .then(m => m.${r.component})
24
25
  }`;
25
26
  const pascal = toPascalCase(r.path);
@@ -28,7 +29,7 @@ ${routes
28
29
  const alias = ` {
29
30
  path: '${pascal}',
30
31
  loadComponent: () =>
31
- import('../features/${r.folder}/${r.fileBase}.component')
32
+ import('${baseImport}')
32
33
  .then(m => m.${r.component})
33
34
  }`;
34
35
  return [base, alias];
@@ -43,3 +44,19 @@ function toPascalCase(value) {
43
44
  return value;
44
45
  return value[0].toUpperCase() + value.slice(1);
45
46
  }
47
+ function buildRelativeImportBase(fromDir, toDir) {
48
+ let relativePath = path_1.default.relative(fromDir, toDir);
49
+ relativePath = toPosixPath(relativePath);
50
+ if (!relativePath.startsWith('.')) {
51
+ relativePath = `./${relativePath}`;
52
+ }
53
+ return relativePath;
54
+ }
55
+ function toPosixPath(value) {
56
+ return value.split(path_1.default.sep).join(path_1.default.posix.sep);
57
+ }
58
+ function ensureRelativeImport(value) {
59
+ if (value.startsWith('.'))
60
+ return value;
61
+ return `./${value}`;
62
+ }
package/dist/index.js CHANGED
@@ -19,15 +19,16 @@ program
19
19
  .command('generate')
20
20
  .description('Generate screen schemas from OpenAPI')
21
21
  .requiredOption('-o, --openapi <path>', 'OpenAPI file')
22
+ .option('--output <path>', 'Output directory for generate-ui (default: ./src/generate-ui or ./generate-ui)')
22
23
  .option('-d, --debug', 'Explain merge decisions')
23
24
  .action(async (options) => {
24
25
  const { telemetry } = program.opts();
25
26
  try {
26
27
  await (0, generate_1.generate)({
27
28
  openapi: options.openapi,
29
+ output: options.output,
28
30
  debug: options.debug,
29
- telemetryEnabled: telemetry,
30
- telemetryCommand: 'generate'
31
+ telemetryEnabled: telemetry
31
32
  });
32
33
  }
33
34
  catch (error) {
@@ -40,8 +41,8 @@ program
40
41
  program
41
42
  .command('angular')
42
43
  .description('Generate Angular code from screen schemas')
43
- .requiredOption('-s, --schemas <path>', 'Directory containing generate-ui (with overlays/)')
44
- .requiredOption('-f, --features <path>', 'Angular features output directory')
44
+ .option('-s, --schemas <path>', 'Directory containing generate-ui (with overlays/)')
45
+ .option('-f, --features <path>', 'Angular features output directory')
45
46
  .action(async (options) => {
46
47
  const { telemetry } = program.opts();
47
48
  try {
@@ -70,29 +71,6 @@ program
70
71
  handleCliError(error);
71
72
  }
72
73
  });
73
- /**
74
- * 4️⃣ Regenerate with safe merge
75
- */
76
- program
77
- .command('regenerate')
78
- .description('Regenerate screen schemas with safe merge')
79
- .requiredOption('-o, --openapi <path>', 'OpenAPI file')
80
- .option('-d, --debug', 'Explain merge decisions')
81
- .action(async (options) => {
82
- const { telemetry } = program.opts();
83
- try {
84
- await (0, generate_1.generate)({
85
- openapi: options.openapi,
86
- debug: options.debug,
87
- telemetryEnabled: telemetry,
88
- telemetryCommand: 'regenerate',
89
- requireSafeRegeneration: true
90
- });
91
- }
92
- catch (error) {
93
- handleCliError(error);
94
- }
95
- });
96
74
  function handleCliError(error) {
97
75
  if (error instanceof Error) {
98
76
  console.error(error.message.replace(/\\n/g, '\n'));
@@ -0,0 +1,38 @@
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.getUserConfigPath = getUserConfigPath;
7
+ exports.loadUserConfig = loadUserConfig;
8
+ exports.saveUserConfig = saveUserConfig;
9
+ exports.updateUserConfig = updateUserConfig;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const path_1 = __importDefault(require("path"));
13
+ function getUserConfigPath() {
14
+ return path_1.default.join(os_1.default.homedir(), '.generateui', 'config.json');
15
+ }
16
+ function loadUserConfig() {
17
+ const configPath = getUserConfigPath();
18
+ if (!fs_1.default.existsSync(configPath))
19
+ return null;
20
+ try {
21
+ const raw = fs_1.default.readFileSync(configPath, 'utf-8');
22
+ return JSON.parse(raw);
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function saveUserConfig(config) {
29
+ const configPath = getUserConfigPath();
30
+ fs_1.default.mkdirSync(path_1.default.dirname(configPath), { recursive: true });
31
+ fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
32
+ }
33
+ function updateUserConfig(updater) {
34
+ const current = loadUserConfig() ?? {};
35
+ const next = updater(current);
36
+ saveUserConfig(next);
37
+ return next;
38
+ }
package/dist/telemetry.js CHANGED
@@ -1,40 +1,117 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sendTelemetry = sendTelemetry;
3
+ exports.trackCommand = trackCommand;
4
+ exports.trackLogin = trackLogin;
5
+ const crypto_1 = require("crypto");
4
6
  const config_1 = require("./runtime/config");
7
+ const user_config_1 = require("./runtime/user-config");
5
8
  const device_1 = require("./license/device");
9
+ const TELEMETRY_URL = process.env.GENERATEUI_TELEMETRY_URL?.trim() ||
10
+ 'https://api.generateui.dev/events';
11
+ const TELEMETRY_TIMEOUT_MS = 1000;
6
12
  function getOsName() {
7
- switch (process.platform) {
8
- case 'darwin':
9
- return 'macos';
10
- case 'win32':
11
- return 'windows';
12
- default:
13
- return 'linux';
14
- }
13
+ return process.platform;
15
14
  }
16
- async function sendTelemetry(command, enabled) {
17
- if (!enabled)
18
- return;
19
- const apiBase = (0, config_1.getApiBaseUrl)();
20
- const device = (0, device_1.loadDeviceIdentity)();
21
- const payload = {
22
- deviceId: device.deviceId,
23
- command,
24
- cliVersion: (0, config_1.getCliVersion)(),
25
- os: getOsName(),
26
- timestamp: new Date().toISOString()
15
+ function loadOrCreateConfig() {
16
+ let config = (0, user_config_1.loadUserConfig)();
17
+ let isNew = false;
18
+ let shouldWrite = false;
19
+ if (!config) {
20
+ isNew = true;
21
+ shouldWrite = true;
22
+ config = {
23
+ installationId: (0, crypto_1.randomUUID)(),
24
+ telemetry: true
25
+ };
26
+ }
27
+ else if (!config.installationId) {
28
+ isNew = true;
29
+ shouldWrite = true;
30
+ config.installationId = (0, crypto_1.randomUUID)();
31
+ if (config.telemetry === undefined) {
32
+ config.telemetry = true;
33
+ }
34
+ }
35
+ else if (config.telemetry === undefined) {
36
+ shouldWrite = true;
37
+ config.telemetry = true;
38
+ }
39
+ if (shouldWrite) {
40
+ (0, user_config_1.saveUserConfig)(config);
41
+ }
42
+ const installationId = config.installationId ?? (0, crypto_1.randomUUID)();
43
+ if (installationId !== config.installationId) {
44
+ shouldWrite = true;
45
+ config.installationId = installationId;
46
+ }
47
+ if (shouldWrite) {
48
+ (0, user_config_1.saveUserConfig)(config);
49
+ }
50
+ return {
51
+ config: {
52
+ installationId,
53
+ telemetry: config.telemetry
54
+ },
55
+ isNew
27
56
  };
57
+ }
58
+ function isTelemetryEnabled(cliEnabled, config) {
59
+ if (!cliEnabled)
60
+ return false;
61
+ return config.telemetry !== false;
62
+ }
63
+ async function sendEvent(payload) {
64
+ const controller = new AbortController();
65
+ const timeout = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
28
66
  try {
29
- await fetch(`${apiBase}/telemetry`, {
67
+ await fetch(TELEMETRY_URL, {
30
68
  method: 'POST',
31
69
  headers: {
32
70
  'Content-Type': 'application/json'
33
71
  },
34
- body: JSON.stringify(payload)
72
+ body: JSON.stringify(payload),
73
+ signal: controller.signal
35
74
  });
36
75
  }
37
76
  catch {
38
77
  // Telemetry must never block execution.
39
78
  }
79
+ finally {
80
+ clearTimeout(timeout);
81
+ }
82
+ }
83
+ async function trackCommand(command, cliEnabled) {
84
+ const { config, isNew } = loadOrCreateConfig();
85
+ const enabled = isTelemetryEnabled(cliEnabled, config);
86
+ if (!enabled)
87
+ return;
88
+ const device = (0, device_1.loadDeviceIdentity)();
89
+ if (isNew) {
90
+ await sendEvent({
91
+ event: 'first_run',
92
+ installationId: config.installationId,
93
+ deviceId: device.deviceId,
94
+ os: getOsName(),
95
+ arch: process.arch,
96
+ cliVersion: (0, config_1.getCliVersion)()
97
+ });
98
+ }
99
+ await sendEvent({
100
+ event: 'command_run',
101
+ installationId: config.installationId,
102
+ command,
103
+ cliVersion: (0, config_1.getCliVersion)()
104
+ });
105
+ }
106
+ async function trackLogin(email, cliEnabled) {
107
+ const { config } = loadOrCreateConfig();
108
+ const enabled = isTelemetryEnabled(cliEnabled, config);
109
+ if (!enabled)
110
+ return;
111
+ await sendEvent({
112
+ event: 'login',
113
+ installationId: config.installationId,
114
+ email: email ?? '',
115
+ cliVersion: (0, config_1.getCliVersion)()
116
+ });
40
117
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "generate-ui-cli",
3
- "version": "1.0.0",
4
- "description": "Generate UI from OpenAPI locally with optional Dev unlocks.",
3
+ "version": "2.1.0",
4
+ "description": "Generate UI from OpenAPI",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",