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.
@@ -7,83 +7,578 @@ exports.angular = angular;
7
7
  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
+ const feature_generator_2 = require("../generators/angular/feature.generator");
10
11
  const routes_generator_1 = require("../generators/angular/routes.generator");
12
+ const menu_generator_1 = require("../generators/angular/menu.generator");
11
13
  const telemetry_1 = require("../telemetry");
12
14
  const user_config_1 = require("../runtime/user-config");
15
+ const permissions_1 = require("../license/permissions");
16
+ const logger_1 = require("../runtime/logger");
17
+ const project_config_1 = require("../runtime/project-config");
13
18
  async function angular(options) {
14
19
  void (0, telemetry_1.trackCommand)('angular', options.telemetryEnabled);
15
- const featuresRoot = resolveFeaturesRoot(options.featuresPath);
16
- const schemasRoot = resolveSchemasRoot(options.schemasPath, featuresRoot);
20
+ let intelligentEnabled = false;
21
+ let subscriptionReason = '';
22
+ try {
23
+ const permissions = await (0, permissions_1.getPermissions)();
24
+ intelligentEnabled = Boolean(permissions.features.intelligentGeneration);
25
+ subscriptionReason = String(permissions.subscription.reason ?? '').trim();
26
+ }
27
+ catch {
28
+ intelligentEnabled = false;
29
+ subscriptionReason = '';
30
+ }
31
+ const projectConfig = (0, project_config_1.findProjectConfig)(process.cwd());
32
+ const configuredFeatures = (0, project_config_1.pickConfiguredPath)(projectConfig.config, 'features');
33
+ const configuredSchemas = (0, project_config_1.pickConfiguredPath)(projectConfig.config, 'schemas') ??
34
+ (0, project_config_1.pickConfiguredPath)(projectConfig.config, 'output');
35
+ const featuresRoot = resolveFeaturesRoot(options.featuresPath, configuredFeatures, projectConfig.configPath);
36
+ const generatedFeaturesRoot = path_1.default.join(featuresRoot, 'generated');
37
+ const overridesFeaturesRoot = path_1.default.join(featuresRoot, 'overrides');
38
+ const schemasRoot = resolveSchemasRoot(options.schemasPath, configuredSchemas, projectConfig.configPath, featuresRoot);
39
+ (0, logger_1.logStep)(`Features output: ${featuresRoot}`);
40
+ (0, logger_1.logDebug)(`Generated features: ${generatedFeaturesRoot}`);
41
+ (0, logger_1.logDebug)(`Overrides: ${overridesFeaturesRoot}`);
42
+ (0, logger_1.logStep)(`Schemas input: ${schemasRoot}`);
17
43
  /**
18
44
  * Onde estão os schemas
19
45
  * Ex: generate-ui
20
46
  */
47
+ if (options.schemasPath && !fs_1.default.existsSync(schemasRoot)) {
48
+ fs_1.default.mkdirSync(schemasRoot, { recursive: true });
49
+ console.log(`ℹ Created generate-ui folder at ${schemasRoot}`);
50
+ }
21
51
  const overlaysDir = path_1.default.join(schemasRoot, 'overlays');
22
- if (!fs_1.default.existsSync(overlaysDir)) {
23
- throw new Error(`Overlays directory not found: ${overlaysDir}`);
52
+ await generateAngularOnce({
53
+ overlaysDir,
54
+ featuresRoot,
55
+ generatedFeaturesRoot,
56
+ overridesFeaturesRoot,
57
+ schemasRoot,
58
+ intelligentEnabled,
59
+ subscriptionReason
60
+ });
61
+ if (!options.watch) {
62
+ (0, logger_1.logTip)('Run with --dev to see detailed logs and file paths.');
63
+ return;
64
+ }
65
+ console.log(`👀 Watching ${overlaysDir} for .screen.json changes...`);
66
+ let debounceTimer = null;
67
+ const rerun = (fileName) => {
68
+ if (debounceTimer)
69
+ clearTimeout(debounceTimer);
70
+ debounceTimer = setTimeout(async () => {
71
+ console.log(`↻ Change detected: ${fileName}`);
72
+ try {
73
+ await generateAngularOnce({
74
+ overlaysDir,
75
+ featuresRoot,
76
+ generatedFeaturesRoot,
77
+ overridesFeaturesRoot,
78
+ schemasRoot,
79
+ intelligentEnabled,
80
+ subscriptionReason
81
+ });
82
+ }
83
+ catch (error) {
84
+ const message = error instanceof Error ? error.message : 'Unexpected error';
85
+ console.error(message);
86
+ }
87
+ }, 180);
88
+ };
89
+ const watcher = fs_1.default.watch(overlaysDir, (event, fileName) => {
90
+ if (!fileName)
91
+ return;
92
+ if (!String(fileName).endsWith('.screen.json'))
93
+ return;
94
+ if (event !== 'change' && event !== 'rename')
95
+ return;
96
+ rerun(String(fileName));
97
+ });
98
+ const closeWatcher = () => {
99
+ watcher.close();
100
+ console.log('\nStopped screen watch.');
101
+ process.exit(0);
102
+ };
103
+ process.on('SIGINT', closeWatcher);
104
+ process.on('SIGTERM', closeWatcher);
105
+ }
106
+ async function generateAngularOnce(options) {
107
+ if (!fs_1.default.existsSync(options.overlaysDir)) {
108
+ throw new Error(`Overlays directory not found: ${options.overlaysDir}\n` +
109
+ 'Run `generate-ui generate` first to create overlays.\n' +
110
+ 'If needed, configure "openapi", "schemas", and "features" in generateui-config.json.');
24
111
  }
112
+ (0, logger_1.logDebug)(`Overlays dir: ${options.overlaysDir}`);
25
113
  const screens = fs_1.default
26
- .readdirSync(overlaysDir)
114
+ .readdirSync(options.overlaysDir)
27
115
  .filter(f => f.endsWith('.screen.json'));
28
- /**
29
- * Onde gerar as features Angular
30
- */
31
- fs_1.default.mkdirSync(featuresRoot, { recursive: true });
116
+ (0, logger_1.logDebug)(`Screens found: ${screens.length}`);
117
+ if (screens.length === 0) {
118
+ throw new Error(`No .screen.json files found in: ${options.overlaysDir}\n` +
119
+ 'Run `generate-ui generate` and then `generate-ui angular`.\n' +
120
+ 'If needed, set "schemas" (or "paths.schemas") in generateui-config.json.');
121
+ }
122
+ fs_1.default.mkdirSync(options.featuresRoot, { recursive: true });
123
+ fs_1.default.mkdirSync(options.generatedFeaturesRoot, { recursive: true });
124
+ fs_1.default.mkdirSync(options.overridesFeaturesRoot, { recursive: true });
32
125
  const routes = [];
126
+ const schemas = [];
127
+ const appRoot = path_1.default.resolve(options.featuresRoot, '..');
128
+ const configInfo = findConfig(appRoot);
129
+ const views = configInfo.config?.views ?? {};
130
+ const generatedSchemasDir = path_1.default.join(options.schemasRoot, 'generated');
33
131
  for (const file of screens) {
34
- const schema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(overlaysDir, file), 'utf-8'));
35
- const route = (0, feature_generator_1.generateFeature)(schema, featuresRoot, schemasRoot);
132
+ let schema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(options.overlaysDir, file), 'utf-8'));
133
+ const generatedPath = path_1.default.join(generatedSchemasDir, file);
134
+ if (fs_1.default.existsSync(generatedPath)) {
135
+ try {
136
+ const generatedSchema = JSON.parse(fs_1.default.readFileSync(generatedPath, 'utf-8'));
137
+ const normalized = enforceScreenShape(schema, generatedSchema);
138
+ schema = normalized.schema;
139
+ for (const warning of normalized.warnings) {
140
+ console.warn(warning);
141
+ }
142
+ }
143
+ catch {
144
+ // Ignore malformed generated schema and keep overlay as source.
145
+ }
146
+ }
147
+ schemas.push({ file, schema });
148
+ }
149
+ const schemaByOpId = new Map();
150
+ for (const { schema } of schemas) {
151
+ const opId = schema?.api?.operationId;
152
+ if (opId)
153
+ schemaByOpId.set(opId, schema);
154
+ }
155
+ for (const { schema } of schemas) {
156
+ const opId = schema?.api?.operationId;
157
+ const view = opId ? views[opId] : undefined;
158
+ if (view) {
159
+ schema.meta = {
160
+ ...(schema.meta ?? {}),
161
+ view
162
+ };
163
+ }
164
+ if (schema?.meta?.intelligent?.kind === 'adminList') {
165
+ if (!options.intelligentEnabled) {
166
+ const reason = String(options.subscriptionReason ?? '').trim();
167
+ (0, logger_1.logTip)(reason
168
+ ? `Intelligent generation is disabled: ${reason}`
169
+ : 'Intelligent generation is disabled. Login and an active subscription are required to generate admin list screens.');
170
+ continue;
171
+ }
172
+ const adminRoute = (0, feature_generator_2.generateAdminFeature)(schema, schemaByOpId, options.featuresRoot, options.generatedFeaturesRoot, options.schemasRoot);
173
+ routes.push(adminRoute);
174
+ continue;
175
+ }
176
+ const route = (0, feature_generator_1.generateFeature)(schema, options.featuresRoot, options.generatedFeaturesRoot, options.schemasRoot);
36
177
  routes.push(route);
37
178
  }
38
- (0, routes_generator_1.generateRoutes)(routes, featuresRoot, schemasRoot);
39
- console.log(`✔ Angular features generated at ${featuresRoot}`);
179
+ migrateLegacyFeatures(routes, options.featuresRoot, options.generatedFeaturesRoot, options.overridesFeaturesRoot);
180
+ syncOverrides(routes, options.generatedFeaturesRoot, options.overridesFeaturesRoot);
181
+ (0, routes_generator_1.generateRoutes)(routes, options.generatedFeaturesRoot, options.overridesFeaturesRoot, options.schemasRoot);
182
+ (0, menu_generator_1.generateMenu)(options.schemasRoot);
183
+ applyAppLayout(options.featuresRoot, options.schemasRoot);
184
+ console.log(`✔ Angular features generated at ${options.featuresRoot}`);
185
+ const overrides = findOverrides(routes, options.overridesFeaturesRoot, options.generatedFeaturesRoot);
186
+ if (overrides.length) {
187
+ console.log('');
188
+ console.log('ℹ Overrides with differences detected:');
189
+ console.log(' Use merge to compare generated (left) vs overrides (right).');
190
+ console.log(' Keep your changes in the right file (overrides).');
191
+ for (const override of overrides) {
192
+ console.log(` - ${override.component} -> generate-ui merge --feature ${override.folder} --file all`);
193
+ }
194
+ console.log('');
195
+ }
40
196
  }
41
- function resolveSchemasRoot(value, featuresRoot) {
42
- if (value) {
43
- return path_1.default.resolve(process.cwd(), value);
197
+ function enforceScreenShape(overlay, generated) {
198
+ const warnings = [];
199
+ const next = overlay ?? {};
200
+ const generatedType = generated?.screen?.type;
201
+ const overlayType = next?.screen?.type;
202
+ if (generatedType &&
203
+ overlayType &&
204
+ String(overlayType) !== String(generatedType)) {
205
+ next.screen = { ...(next.screen ?? {}), type: generatedType };
206
+ warnings.push(`⚠ Ignored overlay change: screen.type is fixed by generation (${generatedType}).`);
44
207
  }
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
- }
208
+ const generatedMode = generated?.screen?.mode;
209
+ const overlayMode = next?.screen?.mode;
210
+ if (generatedMode &&
211
+ overlayMode &&
212
+ String(overlayMode) !== String(generatedMode)) {
213
+ next.screen = { ...(next.screen ?? {}), mode: generatedMode };
214
+ warnings.push(`⚠ Ignored overlay change: screen.mode is fixed by generation (${generatedMode}).`);
215
+ }
216
+ const generatedMethod = generated?.api?.method;
217
+ const overlayMethod = next?.api?.method;
218
+ if (generatedMethod &&
219
+ overlayMethod &&
220
+ String(overlayMethod).toLowerCase() !==
221
+ String(generatedMethod).toLowerCase()) {
222
+ next.api = { ...(next.api ?? {}), method: generatedMethod };
223
+ warnings.push(`⚠ Ignored overlay change: api.method is fixed by generation (${generatedMethod}).`);
224
+ }
225
+ const generatedOpId = generated?.api?.operationId;
226
+ const overlayOpId = next?.api?.operationId;
227
+ if (generatedOpId &&
228
+ overlayOpId &&
229
+ String(overlayOpId) !== String(generatedOpId)) {
230
+ next.api = { ...(next.api ?? {}), operationId: generatedOpId };
231
+ warnings.push(`⚠ Ignored overlay change: api.operationId is fixed by generation (${generatedOpId}).`);
232
+ }
233
+ return { schema: next, warnings };
234
+ }
235
+ function resolveSchemasRoot(value, configured, configPath, featuresRoot) {
236
+ const fromConfig = (0, project_config_1.resolveOptionalPath)(value, configured, configPath);
237
+ if (fromConfig) {
238
+ return fromConfig;
51
239
  }
52
240
  const inferred = inferSchemasRootFromFeatures(featuresRoot);
53
241
  if (inferred)
54
242
  return inferred;
55
243
  return resolveDefaultSchemasRoot();
56
244
  }
57
- function resolveFeaturesRoot(value) {
58
- if (value) {
59
- const resolved = path_1.default.resolve(process.cwd(), value);
60
- const isSrcApp = path_1.default.basename(resolved) === 'app' &&
61
- path_1.default.basename(path_1.default.dirname(resolved)) === 'src';
62
- if (isSrcApp) {
63
- return path_1.default.join(resolved, 'features');
64
- }
65
- return resolved;
245
+ function resolveFeaturesRoot(value, configured, configPath) {
246
+ const fromConfig = (0, project_config_1.resolveOptionalPath)(value, configured, configPath);
247
+ if (fromConfig) {
248
+ return normalizeFeaturesRoot(fromConfig);
66
249
  }
67
250
  const srcAppRoot = path_1.default.resolve(process.cwd(), 'src', 'app');
68
- if (!fs_1.default.existsSync(srcAppRoot)) {
69
- throw new Error('Default features path not found: ./src/app. Provide --features /path/to/src/app (or /path/to/src/app/features)');
251
+ if (fs_1.default.existsSync(srcAppRoot)) {
252
+ return path_1.default.join(srcAppRoot, 'features');
70
253
  }
71
- return path_1.default.join(srcAppRoot, 'features');
254
+ throw new Error('Default features path not found.\n' +
255
+ 'Use --features <path> or set "features" (or "paths.features") in generateui-config.json.');
72
256
  }
73
257
  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;
258
+ const srcCandidate = path_1.default.resolve(featuresRoot, '..', '..', 'generate-ui');
259
+ if (fs_1.default.existsSync(path_1.default.join(srcCandidate, 'overlays'))) {
260
+ return srcCandidate;
261
+ }
262
+ const rootCandidate = path_1.default.resolve(featuresRoot, '../../..', 'generate-ui');
263
+ if (fs_1.default.existsSync(path_1.default.join(rootCandidate, 'overlays'))) {
264
+ return rootCandidate;
77
265
  }
78
266
  return null;
79
267
  }
80
268
  function resolveDefaultSchemasRoot() {
269
+ const userConfig = (0, user_config_1.loadUserConfig)();
270
+ if (userConfig?.lastSchemasPath &&
271
+ fs_1.default.existsSync(path_1.default.join(userConfig.lastSchemasPath, 'overlays'))) {
272
+ return userConfig.lastSchemasPath;
273
+ }
81
274
  const cwd = process.cwd();
82
275
  if (fs_1.default.existsSync(path_1.default.join(cwd, 'src'))) {
83
276
  return path_1.default.join(cwd, 'src', 'generate-ui');
84
277
  }
85
- if (fs_1.default.existsSync(path_1.default.join(cwd, 'frontend', 'src'))) {
86
- return path_1.default.join(cwd, 'frontend', 'src', 'generate-ui');
87
- }
88
278
  return path_1.default.join(cwd, 'generate-ui');
89
279
  }
280
+ function normalizeFeaturesRoot(value) {
281
+ const isSrcApp = path_1.default.basename(value) === 'app' &&
282
+ path_1.default.basename(path_1.default.dirname(value)) === 'src';
283
+ if (isSrcApp)
284
+ return path_1.default.join(value, 'features');
285
+ return value;
286
+ }
287
+ function findOverrides(routes, overridesRoot, generatedRoot) {
288
+ const results = [];
289
+ for (const route of routes) {
290
+ const overridePath = path_1.default.join(overridesRoot, route.folder, `${route.fileBase}.component.ts`);
291
+ const generatedPath = path_1.default.join(generatedRoot, route.folder, `${route.fileBase}.component.ts`);
292
+ if (!fs_1.default.existsSync(overridePath))
293
+ continue;
294
+ if (!fs_1.default.existsSync(generatedPath))
295
+ continue;
296
+ const overrideRaw = fs_1.default.readFileSync(overridePath, 'utf-8');
297
+ const generatedRaw = fs_1.default.readFileSync(generatedPath, 'utf-8');
298
+ if (overrideRaw !== generatedRaw) {
299
+ results.push({
300
+ folder: route.folder,
301
+ component: route.component,
302
+ generatedPath,
303
+ overridePath
304
+ });
305
+ }
306
+ }
307
+ return results;
308
+ }
309
+ function syncOverrides(routes, generatedRoot, overridesRoot) {
310
+ const overridesEmpty = fs_1.default.existsSync(overridesRoot) &&
311
+ fs_1.default.readdirSync(overridesRoot).length === 0;
312
+ if (overridesEmpty && fs_1.default.existsSync(generatedRoot)) {
313
+ fs_1.default.cpSync(generatedRoot, overridesRoot, { recursive: true });
314
+ console.log('ℹ Seeded overrides from generated (initial sync).');
315
+ return;
316
+ }
317
+ for (const route of routes) {
318
+ const sourceDir = path_1.default.join(generatedRoot, route.folder);
319
+ const targetDir = path_1.default.join(overridesRoot, route.folder);
320
+ if (!fs_1.default.existsSync(sourceDir))
321
+ continue;
322
+ if (fs_1.default.existsSync(targetDir))
323
+ continue;
324
+ fs_1.default.mkdirSync(path_1.default.dirname(targetDir), { recursive: true });
325
+ fs_1.default.cpSync(sourceDir, targetDir, { recursive: true });
326
+ }
327
+ }
328
+ function migrateLegacyFeatures(routes, featuresRoot, generatedRoot, overridesRoot) {
329
+ let migrated = false;
330
+ for (const route of routes) {
331
+ const legacyDir = path_1.default.join(featuresRoot, route.folder);
332
+ const generatedDir = path_1.default.join(generatedRoot, route.folder);
333
+ const overrideDir = path_1.default.join(overridesRoot, route.folder);
334
+ if (!fs_1.default.existsSync(legacyDir))
335
+ continue;
336
+ if (fs_1.default.existsSync(generatedDir) || fs_1.default.existsSync(overrideDir)) {
337
+ continue;
338
+ }
339
+ fs_1.default.mkdirSync(path_1.default.dirname(generatedDir), { recursive: true });
340
+ fs_1.default.cpSync(legacyDir, generatedDir, { recursive: true });
341
+ fs_1.default.cpSync(legacyDir, overrideDir, { recursive: true });
342
+ migrated = true;
343
+ }
344
+ if (migrated) {
345
+ console.log('ℹ Legacy feature folders detected. Seeded generated/overrides.');
346
+ }
347
+ }
348
+ function applyAppLayout(featuresRoot, schemasRoot) {
349
+ const appRoot = path_1.default.resolve(featuresRoot, '..');
350
+ const configInfo = findConfig(appRoot);
351
+ const config = configInfo.config;
352
+ if (configInfo.configPath) {
353
+ (0, logger_1.logDebug)(`Config path: ${configInfo.configPath}`);
354
+ }
355
+ if (config) {
356
+ const resolvedTitle = config.appTitle ?? 'Generate UI';
357
+ const resolvedRoute = config.defaultRoute ?? '(not set)';
358
+ const resolvedInject = config.menu?.autoInject !== false;
359
+ console.log('✅ GenerateUI config detected');
360
+ console.log('');
361
+ console.log(` 🏷️ appTitle: "${resolvedTitle}"`);
362
+ console.log(` ➡️ defaultRoute: ${resolvedRoute}`);
363
+ console.log(` 🧭 menu.autoInject: ${resolvedInject}`);
364
+ console.log(' ✍️ Edit generateui-config.json before running `generate-ui generate` when you want to change title, route, views, or menu behavior.');
365
+ console.log(' 🧩 menu overrides: edit generate-ui/menu.overrides.json to customize labels, groups, and order');
366
+ console.log(' (this file is created once and never overwritten)');
367
+ console.log('');
368
+ }
369
+ else {
370
+ console.log('ℹ️ No generateui-config.json found. Using defaults.');
371
+ console.log('');
372
+ console.log(' ✨ To customize, create/edit generateui-config.json at your project root before running `generate-ui generate`.');
373
+ console.log(' 🧩 To customize the menu, edit generate-ui/menu.overrides.json (created on first generate).');
374
+ console.log('');
375
+ }
376
+ if (config?.defaultRoute) {
377
+ injectDefaultRoute(appRoot, config.defaultRoute);
378
+ }
379
+ ensureBaseStyles(appRoot);
380
+ const autoInject = config?.menu?.autoInject !== false;
381
+ if (autoInject) {
382
+ const appTitle = config?.appTitle || 'Generate UI';
383
+ injectMenuLayout(appRoot, appTitle, schemasRoot);
384
+ }
385
+ }
386
+ function findConfig(startDir) {
387
+ let dir = startDir;
388
+ let config = null;
389
+ let configPath = null;
390
+ const root = path_1.default.parse(dir).root;
391
+ while (true) {
392
+ const candidate = path_1.default.join(dir, 'generateui-config.json');
393
+ if (!config && fs_1.default.existsSync(candidate)) {
394
+ try {
395
+ config = JSON.parse(fs_1.default.readFileSync(candidate, 'utf-8'));
396
+ configPath = candidate;
397
+ }
398
+ catch {
399
+ config = null;
400
+ }
401
+ }
402
+ if (dir === root)
403
+ break;
404
+ dir = path_1.default.dirname(dir);
405
+ }
406
+ return { config, configPath };
407
+ }
408
+ function injectDefaultRoute(appRoot, value) {
409
+ const routesPath = path_1.default.join(appRoot, 'app.routes.ts');
410
+ if (!fs_1.default.existsSync(routesPath)) {
411
+ (0, logger_1.logDebug)(`Skip defaultRoute: ${routesPath} not found`);
412
+ return;
413
+ }
414
+ let content = fs_1.default.readFileSync(routesPath, 'utf-8');
415
+ const route = normalizeRoutePath(value);
416
+ const insertion = ` { path: '', pathMatch: 'full', redirectTo: '${route}' },\n`;
417
+ if (!content.trim().length) {
418
+ const template = `import { Routes } from '@angular/router'\nimport { generatedRoutes } from '../generate-ui/routes.gen'\n\nexport const routes: Routes = [\n${insertion} ...generatedRoutes\n]\n`;
419
+ fs_1.default.writeFileSync(routesPath, template);
420
+ (0, logger_1.logDebug)(`Default route injected (created): ${routesPath}`);
421
+ return;
422
+ }
423
+ if (!content.match(/export const routes\s*:\s*Routes\s*=/)) {
424
+ if (!content.match(/import\s+\{\s*Routes\s*\}\s+from\s+['"]@angular\/router['"]/)) {
425
+ content = `import { Routes } from '@angular/router'\n${content}`;
426
+ }
427
+ content = content.replace(/export const routes\s*=/, 'export const routes: Routes =');
428
+ }
429
+ if (content.includes('generatedRoutes') &&
430
+ !content.match(/import\s+\{\s*generatedRoutes\s*\}\s+from\s+['"].*generate-ui\/routes\.gen['"]/)) {
431
+ content =
432
+ `import { generatedRoutes } from '../generate-ui/routes.gen'\n` +
433
+ content;
434
+ }
435
+ content = content.replace(/export const routes\s*=\s*\[/, match => `${match}\n${insertion}`);
436
+ const emptyRedirect = /path:\s*['"]\s*['"]\s*,\s*pathMatch:\s*['"]full['"]\s*,\s*redirectTo:\s*['"]\s*['"]/;
437
+ if (emptyRedirect.test(content)) {
438
+ content = content.replace(emptyRedirect, `path: '', pathMatch: 'full', redirectTo: '${route}'`);
439
+ }
440
+ fs_1.default.writeFileSync(routesPath, content);
441
+ (0, logger_1.logDebug)(`Default route injected (updated): ${routesPath}`);
442
+ }
443
+ function injectMenuLayout(appRoot, appTitle, schemasRoot) {
444
+ const appHtmlPath = path_1.default.join(appRoot, 'app.html');
445
+ const appCssPath = path_1.default.join(appRoot, 'app.css');
446
+ const appTsPath = path_1.default.join(appRoot, 'app.ts');
447
+ if (!fs_1.default.existsSync(appHtmlPath) ||
448
+ !fs_1.default.existsSync(appCssPath) ||
449
+ !fs_1.default.existsSync(appTsPath)) {
450
+ (0, logger_1.logDebug)('Skip menu injection: app.html/app.css/app.ts not found');
451
+ return;
452
+ }
453
+ const htmlRaw = fs_1.default.readFileSync(appHtmlPath, 'utf-8');
454
+ if (htmlRaw.includes('<ui-menu')) {
455
+ let updatedHtml = htmlRaw;
456
+ updatedHtml = updatedHtml.replace(/<ui-menu(?![^>]*\[\s*title\s*\])[\\s>]/, '<ui-menu [title]="appTitle()">');
457
+ if (updatedHtml !== htmlRaw) {
458
+ fs_1.default.writeFileSync(appHtmlPath, updatedHtml);
459
+ (0, logger_1.logDebug)(`Updated ui-menu title binding: ${appHtmlPath}`);
460
+ }
461
+ }
462
+ else {
463
+ const normalized = htmlRaw.replace(/\s+/g, '');
464
+ const isDefaultOutlet = normalized === '<router-outlet></router-outlet>' ||
465
+ normalized === '<router-outlet/>' ||
466
+ normalized === '<router-outlet/>';
467
+ if (!isDefaultOutlet)
468
+ return;
469
+ 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`;
470
+ fs_1.default.writeFileSync(appHtmlPath, newHtml);
471
+ (0, logger_1.logDebug)(`Injected menu layout into: ${appHtmlPath}`);
472
+ }
473
+ const cssRaw = fs_1.default.readFileSync(appCssPath, 'utf-8');
474
+ if (!cssRaw.includes('.app-shell')) {
475
+ const shellCss = `:host {\n display: block;\n min-height: 100vh;\n color: #0f172a;\n background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 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`;
476
+ fs_1.default.writeFileSync(appCssPath, cssRaw.trim().length ? `${cssRaw.trim()}\n\n${shellCss}` : shellCss);
477
+ (0, logger_1.logDebug)(`Injected menu shell styles into: ${appCssPath}`);
478
+ }
479
+ let tsRaw = fs_1.default.readFileSync(appTsPath, 'utf-8');
480
+ if (!tsRaw.includes('UiMenuComponent')) {
481
+ tsRaw = tsRaw.replace(/import\s+\{\s*([^}]+)\s*\}\s+from\s+['"]@angular\/router['"];/, (match) => `${match}\nimport { UiMenuComponent } from './ui/ui-menu/ui-menu.component';`);
482
+ }
483
+ if (tsRaw.includes('imports: [')) {
484
+ tsRaw = tsRaw.replace(/imports:\s*\[/, match => `${match}RouterOutlet, UiMenuComponent, `);
485
+ tsRaw = tsRaw.replace(/UiMenuComponent,\s*UiMenuComponent,\s*/g, 'UiMenuComponent, ');
486
+ tsRaw = tsRaw.replace(/RouterOutlet,\s*RouterOutlet,\s*/g, 'RouterOutlet, ');
487
+ tsRaw = tsRaw.replace(/UiMenuComponent,\s*RouterOutlet,\s*UiMenuComponent,/g, 'UiMenuComponent, ');
488
+ }
489
+ if (tsRaw.includes('appTitle')) {
490
+ tsRaw = tsRaw.replace(/appTitle\s*=\s*signal\('([^']*)'\)/, `appTitle = signal('${escapeString(appTitle)}')`);
491
+ }
492
+ else {
493
+ if (!tsRaw.match(/import\s+\{\s*[^}]*\bsignal\b[^}]*\}\s+from\s+['"]@angular\/core['"]/)) {
494
+ tsRaw = tsRaw.replace(/import\s+\{\s*([^}]+)\s*\}\s+from\s+['"]@angular\/core['"];?/, (match, imports) => {
495
+ if (imports.includes('signal'))
496
+ return match;
497
+ return `import { ${imports.trim()}, signal } from '@angular/core';`;
498
+ });
499
+ }
500
+ tsRaw = tsRaw.replace(/export class App\s*\{\s*/, match => `${match}\n protected readonly appTitle = signal('${escapeString(appTitle)}');\n`);
501
+ }
502
+ // Remove legacy runtime config loader if present.
503
+ if (tsRaw.includes('loadRuntimeConfig')) {
504
+ tsRaw = tsRaw.replace(/\\s*constructor\\(\\)\\s*\\{[\\s\\S]*?\\}\\s*/m, '\n');
505
+ tsRaw = tsRaw.replace(/\\s*private\\s+loadRuntimeConfig\\(\\)\\s*\\{[\\s\\S]*?\\}\\s*/m, '\n');
506
+ }
507
+ fs_1.default.writeFileSync(appTsPath, tsRaw);
508
+ (0, logger_1.logDebug)(`Updated app title/menu imports: ${appTsPath}`);
509
+ const menuComponentPath = path_1.default.join(appRoot, 'ui', 'ui-menu', 'ui-menu.component.ts');
510
+ if (fs_1.default.existsSync(menuComponentPath)) {
511
+ // Touch to keep consistent in case it was generated before config title existed.
512
+ void schemasRoot;
513
+ }
514
+ }
515
+ function ensureBaseStyles(appRoot) {
516
+ const workspaceRoot = findAngularWorkspaceRoot(appRoot) ?? path_1.default.resolve(appRoot, '..');
517
+ const stylesPath = path_1.default.join(workspaceRoot, 'src', 'styles.css');
518
+ if (fs_1.default.existsSync(stylesPath))
519
+ return;
520
+ if (fs_1.default.existsSync(path_1.default.join(workspaceRoot, 'src', 'styles.scss')))
521
+ return;
522
+ const styles = `@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&display=swap');
523
+
524
+ :root {
525
+ --bg-page: #f8fafc;
526
+ --bg-surface: #ffffff;
527
+ --bg-ink: #0f172a;
528
+ --color-text: #0f172a;
529
+ --color-muted: #64748b;
530
+ --color-border: #d1d5db;
531
+ --color-primary: #3b82f6;
532
+ --color-primary-strong: #1d4ed8;
533
+ --color-primary-soft: rgba(59, 130, 246, 0.14);
534
+ --color-accent: #0ea5e9;
535
+ --color-accent-strong: #0284c7;
536
+ --color-accent-soft: rgba(14, 165, 233, 0.14);
537
+ --shadow-card: 0 4px 16px rgba(15, 23, 42, 0.08);
538
+ }
539
+
540
+ body {
541
+ margin: 0;
542
+ background: var(--bg-page);
543
+ color: var(--color-text);
544
+ font-family: "DM Sans", "Segoe UI", sans-serif;
545
+ }
546
+ `;
547
+ fs_1.default.writeFileSync(stylesPath, styles);
548
+ }
549
+ function findAngularWorkspaceRoot(startDir) {
550
+ let dir = startDir;
551
+ const root = path_1.default.parse(dir).root;
552
+ while (true) {
553
+ const candidate = path_1.default.join(dir, 'angular.json');
554
+ if (fs_1.default.existsSync(candidate))
555
+ return dir;
556
+ if (dir === root)
557
+ return null;
558
+ dir = path_1.default.dirname(dir);
559
+ }
560
+ }
561
+ function escapeString(value) {
562
+ return String(value).replace(/'/g, "\\'");
563
+ }
564
+ function normalizeRoutePath(value) {
565
+ const trimmed = String(value ?? '').trim();
566
+ if (!trimmed)
567
+ return trimmed;
568
+ if (trimmed.includes('/'))
569
+ return trimmed.replace(/^\//, '');
570
+ const pascal = toPascalCase(trimmed);
571
+ return toRouteSegment(pascal);
572
+ }
573
+ function toRouteSegment(value) {
574
+ if (!value)
575
+ return value;
576
+ return value[0].toLowerCase() + value.slice(1);
577
+ }
578
+ function toPascalCase(value) {
579
+ return String(value)
580
+ .split(/[^a-zA-Z0-9]+/)
581
+ .filter(Boolean)
582
+ .map(part => part[0].toUpperCase() + part.slice(1))
583
+ .join('');
584
+ }