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.
@@ -0,0 +1,165 @@
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.generateMenu = generateMenu;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ function generateMenu(schemasRoot) {
10
+ const menu = loadMenuConfig(schemasRoot) ??
11
+ buildMenuFromRoutes(loadRoutesConfig(schemasRoot)) ?? {
12
+ groups: [],
13
+ ungrouped: []
14
+ };
15
+ const out = path_1.default.join(schemasRoot, 'menu.gen.ts');
16
+ const content = `
17
+ export type GeneratedMenuItem = {
18
+ id: string
19
+ label: string
20
+ route: string
21
+ hidden?: boolean
22
+ icon?: string
23
+ }
24
+
25
+ export type GeneratedMenuGroup = {
26
+ id: string
27
+ label: string
28
+ items: GeneratedMenuItem[]
29
+ hidden?: boolean
30
+ }
31
+
32
+ export type GeneratedMenu = {
33
+ groups: GeneratedMenuGroup[]
34
+ ungrouped: GeneratedMenuItem[]
35
+ }
36
+
37
+ export const generatedMenu: GeneratedMenu = ${JSON.stringify(normalizeMenu(menu), null, 2)}
38
+ `;
39
+ fs_1.default.writeFileSync(out, content.trimStart());
40
+ }
41
+ function loadMenuConfig(schemasRoot) {
42
+ const overridePath = path_1.default.join(schemasRoot, 'menu.overrides.json');
43
+ const basePath = path_1.default.join(schemasRoot, 'menu.json');
44
+ if (fs_1.default.existsSync(overridePath)) {
45
+ const override = JSON.parse(fs_1.default.readFileSync(overridePath, 'utf-8'));
46
+ const hasOverride = Array.isArray(override?.groups) &&
47
+ override.groups.length > 0
48
+ ? true
49
+ : Array.isArray(override?.ungrouped) &&
50
+ override.ungrouped.length > 0;
51
+ if (hasOverride) {
52
+ return override;
53
+ }
54
+ }
55
+ if (fs_1.default.existsSync(basePath)) {
56
+ return JSON.parse(fs_1.default.readFileSync(basePath, 'utf-8'));
57
+ }
58
+ return null;
59
+ }
60
+ function loadRoutesConfig(schemasRoot) {
61
+ const routesPath = path_1.default.join(schemasRoot, 'routes.json');
62
+ if (!fs_1.default.existsSync(routesPath))
63
+ return null;
64
+ try {
65
+ return JSON.parse(fs_1.default.readFileSync(routesPath, 'utf-8'));
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ function normalizeMenu(value) {
72
+ return {
73
+ groups: Array.isArray(value?.groups)
74
+ ? value.groups.map(normalizeGroup)
75
+ : [],
76
+ ungrouped: Array.isArray(value?.ungrouped)
77
+ ? value.ungrouped.map(normalizeItem)
78
+ : []
79
+ };
80
+ }
81
+ function normalizeGroup(value) {
82
+ return {
83
+ id: String(value?.id ?? ''),
84
+ label: String(value?.label ?? ''),
85
+ hidden: Boolean(value?.hidden) || undefined,
86
+ items: Array.isArray(value?.items)
87
+ ? value.items.map(normalizeItem)
88
+ : []
89
+ };
90
+ }
91
+ function normalizeItem(value) {
92
+ return {
93
+ id: String(value?.id ?? ''),
94
+ label: String(value?.label ?? ''),
95
+ route: String(value?.route ?? ''),
96
+ hidden: Boolean(value?.hidden) || undefined,
97
+ icon: value?.icon ? String(value.icon) : undefined
98
+ };
99
+ }
100
+ function buildMenuFromRoutes(routes) {
101
+ if (!Array.isArray(routes) || routes.length === 0)
102
+ return null;
103
+ const groups = [];
104
+ const ungrouped = [];
105
+ const groupMap = new Map();
106
+ for (const route of routes) {
107
+ if (!route?.path || !route?.operationId)
108
+ continue;
109
+ const item = {
110
+ id: String(route.operationId),
111
+ label: toLabel(String(route.label ?? route.operationId)),
112
+ route: normalizeRoutePath(String(route.path ?? route.operationId ?? ''))
113
+ };
114
+ const rawGroup = route.group ? String(route.group) : '';
115
+ if (!rawGroup) {
116
+ ungrouped.push(item);
117
+ continue;
118
+ }
119
+ const groupId = toKebab(rawGroup);
120
+ let group = groupMap.get(groupId);
121
+ if (!group) {
122
+ group = {
123
+ id: groupId,
124
+ label: toLabel(rawGroup),
125
+ items: []
126
+ };
127
+ groupMap.set(groupId, group);
128
+ groups.push(group);
129
+ }
130
+ group.items.push(item);
131
+ }
132
+ return { groups, ungrouped };
133
+ }
134
+ function toKebab(value) {
135
+ return String(value)
136
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
137
+ .replace(/[_\\s]+/g, '-')
138
+ .toLowerCase();
139
+ }
140
+ function toLabel(value) {
141
+ return String(value)
142
+ .replace(/[_-]/g, ' ')
143
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
144
+ .replace(/\\b\\w/g, char => char.toUpperCase());
145
+ }
146
+ function normalizeRoutePath(value) {
147
+ if (!value)
148
+ return value;
149
+ if (value.includes('/'))
150
+ return value.replace(/^\//, '');
151
+ const pascal = toPascalCase(value);
152
+ return toRouteSegment(pascal);
153
+ }
154
+ function toRouteSegment(value) {
155
+ if (!value)
156
+ return value;
157
+ return value[0].toLowerCase() + value.slice(1);
158
+ }
159
+ function toPascalCase(value) {
160
+ return String(value)
161
+ .split(/[^a-zA-Z0-9]+/)
162
+ .filter(Boolean)
163
+ .map(part => part[0].toUpperCase() + part.slice(1))
164
+ .join('');
165
+ }
@@ -6,9 +6,10 @@ 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, schemasRoot) {
9
+ function generateRoutes(routes, generatedRoot, overridesRoot, schemasRoot) {
10
10
  const out = path_1.default.join(schemasRoot, 'routes.gen.ts');
11
- const featuresImportBase = buildRelativeImportBase(schemasRoot, featuresRoot);
11
+ const generatedImportBase = buildRelativeImportBase(schemasRoot, generatedRoot);
12
+ const overridesImportBase = buildRelativeImportBase(schemasRoot, overridesRoot);
12
13
  fs_1.default.mkdirSync(path_1.default.dirname(out), { recursive: true });
13
14
  const content = `
14
15
  import { Routes } from '@angular/router'
@@ -16,7 +17,11 @@ import { Routes } from '@angular/router'
16
17
  export const generatedRoutes: Routes = [
17
18
  ${routes
18
19
  .flatMap(r => {
19
- const baseImport = ensureRelativeImport(toPosixPath(path_1.default.join(featuresImportBase, r.folder, `${r.fileBase}.component`)));
20
+ const overridePath = path_1.default.join(overridesRoot, r.folder, `${r.fileBase}.component.ts`);
21
+ const importBase = fs_1.default.existsSync(overridePath)
22
+ ? overridesImportBase
23
+ : generatedImportBase;
24
+ const baseImport = ensureRelativeImport(toPosixPath(path_1.default.join(importBase, r.folder, `${r.fileBase}.component`)));
20
25
  const base = ` {
21
26
  path: '${r.path}',
22
27
  loadComponent: () =>
@@ -19,13 +19,13 @@ function inferScreen(method, hasInput) {
19
19
  }
20
20
  function inferActions(method, hasInput) {
21
21
  if (method === 'get' && hasInput) {
22
- return { primary: { type: 'submit', label: 'Buscar' } };
22
+ return { primary: { type: 'submit', label: 'Search' } };
23
23
  }
24
24
  if (method === 'post') {
25
- return { primary: { type: 'submit', label: 'Criar' } };
25
+ return { primary: { type: 'submit', label: 'Create' } };
26
26
  }
27
27
  if (method === 'put' || method === 'patch') {
28
- return { primary: { type: 'submit', label: 'Salvar' } };
28
+ return { primary: { type: 'submit', label: 'Save' } };
29
29
  }
30
30
  return {};
31
31
  }
@@ -49,6 +49,15 @@ function generateScreen(endpoint, api) {
49
49
  pathParams.length > 0;
50
50
  const openapiVersion = api?.info?.version || 'unknown';
51
51
  const screenMeta = buildMeta(endpoint.operationId, 'api', openapiVersion);
52
+ const columnKeys = method === 'get'
53
+ ? inferResponseColumns(endpoint)
54
+ : [];
55
+ const responseFormat = inferResponseFormat(endpoint);
56
+ const columns = columnKeys.map(key => ({
57
+ key,
58
+ label: toLabel(key),
59
+ visible: true
60
+ }));
52
61
  return {
53
62
  meta: screenMeta,
54
63
  entity: endpoint.summary
@@ -70,9 +79,90 @@ function generateScreen(endpoint, api) {
70
79
  layout: { type: 'single' },
71
80
  fields: decorateFields(fields, 'body', openapiVersion),
72
81
  actions: inferActions(method, hasInput),
73
- data: {}
82
+ response: responseFormat
83
+ ? { format: responseFormat }
84
+ : undefined,
85
+ data: {
86
+ table: {
87
+ columns
88
+ }
89
+ }
74
90
  };
75
91
  }
92
+ function inferResponseColumns(endpoint) {
93
+ const schema = getPrimaryResponseSchema(endpoint);
94
+ if (!schema)
95
+ return [];
96
+ return inferColumnsFromSchema(schema);
97
+ }
98
+ function inferResponseFormat(endpoint) {
99
+ const schema = getPrimaryResponseSchema(endpoint);
100
+ if (!schema)
101
+ return null;
102
+ if (!hasResponseData(schema))
103
+ return null;
104
+ return 'table';
105
+ }
106
+ function getPrimaryResponseSchema(endpoint) {
107
+ const responses = endpoint?.responses ?? {};
108
+ const candidate = responses['200'] ||
109
+ responses['201'] ||
110
+ responses.default ||
111
+ Object.values(responses)[0];
112
+ return candidate?.content?.['application/json']?.schema ?? null;
113
+ }
114
+ function hasResponseData(schema) {
115
+ if (!schema)
116
+ return false;
117
+ if (Array.isArray(schema.allOf)) {
118
+ return schema.allOf.some((entry) => hasResponseData(entry));
119
+ }
120
+ if (Array.isArray(schema.anyOf)) {
121
+ return schema.anyOf.some((entry) => hasResponseData(entry));
122
+ }
123
+ if (Array.isArray(schema.oneOf)) {
124
+ return schema.oneOf.some((entry) => hasResponseData(entry));
125
+ }
126
+ if (schema.type === 'array')
127
+ return true;
128
+ if (schema.type === 'object' &&
129
+ (schema.properties || schema.additionalProperties)) {
130
+ return true;
131
+ }
132
+ return inferColumnsFromSchema(schema).length > 0;
133
+ }
134
+ function inferColumnsFromSchema(schema) {
135
+ if (!schema)
136
+ return [];
137
+ if (Array.isArray(schema.allOf)) {
138
+ for (const entry of schema.allOf) {
139
+ const columns = inferColumnsFromSchema(entry);
140
+ if (columns.length)
141
+ return columns;
142
+ }
143
+ }
144
+ if (schema.type === 'array') {
145
+ return inferColumnsFromSchema(schema.items);
146
+ }
147
+ if (schema.type === 'object' && schema.properties) {
148
+ const commonKeys = [
149
+ 'data',
150
+ 'items',
151
+ 'results',
152
+ 'list',
153
+ 'records',
154
+ 'products'
155
+ ];
156
+ for (const key of commonKeys) {
157
+ const nested = schema.properties[key];
158
+ const columns = inferColumnsFromSchema(nested);
159
+ if (columns.length)
160
+ return columns;
161
+ }
162
+ return Object.keys(schema.properties);
163
+ }
164
+ return [];
165
+ }
76
166
  function extractQueryParams(endpoint, api) {
77
167
  const params = endpoint?.parameters ?? [];
78
168
  return params
@@ -110,11 +200,16 @@ function extractPathParams(path, api) {
110
200
  }));
111
201
  }
112
202
  function toLabel(value) {
113
- return String(value)
203
+ return stripDiacritics(String(value))
114
204
  .replace(/[_-]/g, ' ')
115
205
  .replace(/([a-z])([A-Z])/g, '$1 $2')
116
206
  .replace(/\b\w/g, char => char.toUpperCase());
117
207
  }
208
+ function stripDiacritics(value) {
209
+ return value
210
+ .normalize('NFD')
211
+ .replace(/[\u0300-\u036f]/g, '');
212
+ }
118
213
  function buildMeta(id, source, openapiVersion) {
119
214
  return {
120
215
  id,
@@ -10,6 +10,10 @@ const PRESENTATION_KEYS = [
10
10
  'group',
11
11
  'hidden'
12
12
  ];
13
+ const COLUMN_PRESENTATION_KEYS = [
14
+ 'label',
15
+ 'visible'
16
+ ];
13
17
  function mergeScreen(nextScreen, overlay, prevGenerated, options) {
14
18
  const debug = [];
15
19
  if (!overlay) {
@@ -25,7 +29,9 @@ function mergeScreen(nextScreen, overlay, prevGenerated, options) {
25
29
  entity: normalizedOverlay.entity ?? normalizedNext.entity,
26
30
  screen: normalizedOverlay.screen ?? normalizedNext.screen,
27
31
  layout: normalizedOverlay.layout ?? normalizedNext.layout,
28
- actions: mergeActions(normalizedNext.actions, normalizedOverlay.actions)
32
+ actions: mergeActions(normalizedNext.actions, normalizedOverlay.actions),
33
+ response: mergeResponse(normalizedNext.response, normalizedOverlay.response),
34
+ data: mergeData(normalizedNext.data, normalizedOverlay.data)
29
35
  };
30
36
  merged.api = {
31
37
  ...normalizedNext.api,
@@ -46,6 +52,62 @@ function mergeActions(nextActions, overlayActions) {
46
52
  }
47
53
  return merged;
48
54
  }
55
+ function mergeResponse(nextResponse, overlayResponse) {
56
+ if (!overlayResponse)
57
+ return nextResponse;
58
+ const allowed = ['table', 'cards', 'raw'];
59
+ const candidate = String(overlayResponse?.format || '')
60
+ .trim()
61
+ .toLowerCase();
62
+ if (!allowed.includes(candidate))
63
+ return nextResponse;
64
+ return {
65
+ ...(nextResponse || {}),
66
+ format: candidate
67
+ };
68
+ }
69
+ function mergeData(nextData, overlayData) {
70
+ const nextColumns = nextData?.table?.columns;
71
+ const overlayColumns = overlayData?.table?.columns;
72
+ if (!Array.isArray(nextColumns)) {
73
+ return nextData ?? {};
74
+ }
75
+ const overlayMap = new Map();
76
+ if (Array.isArray(overlayColumns)) {
77
+ for (const entry of overlayColumns) {
78
+ if (!entry || typeof entry !== 'object')
79
+ continue;
80
+ const key = String(entry.key ?? entry.id ?? entry.column ?? '').trim();
81
+ if (!key)
82
+ continue;
83
+ overlayMap.set(key, entry);
84
+ }
85
+ }
86
+ const mergedColumns = nextColumns.map((entry) => {
87
+ if (!entry || typeof entry !== 'object')
88
+ return entry;
89
+ const key = String(entry.key ?? entry.id ?? entry.column ?? '').trim();
90
+ if (!key)
91
+ return entry;
92
+ const overlay = overlayMap.get(key);
93
+ if (!overlay)
94
+ return entry;
95
+ const merged = { ...entry };
96
+ for (const prop of COLUMN_PRESENTATION_KEYS) {
97
+ if (overlay[prop] !== undefined) {
98
+ merged[prop] = overlay[prop];
99
+ }
100
+ }
101
+ return merged;
102
+ });
103
+ return {
104
+ ...(nextData || {}),
105
+ table: {
106
+ ...(nextData?.table || {}),
107
+ columns: mergedColumns
108
+ }
109
+ };
110
+ }
49
111
  function mergeFieldList(nextFields, overlayFields, prevFields, options, debug, scope) {
50
112
  const nextMap = indexById(nextFields, scope);
51
113
  const overlayMap = indexById(overlayFields, scope);
@@ -80,20 +142,6 @@ function mergeFieldList(nextFields, overlayFields, prevFields, options, debug, s
80
142
  for (const [id, nextField] of nextMap.entries()) {
81
143
  if (used.has(id))
82
144
  continue;
83
- const prevField = prevMap.get(id);
84
- if (prevField && !overlayMap.has(id)) {
85
- result.push({
86
- ...nextField,
87
- hidden: true,
88
- meta: {
89
- ...mergeMeta(nextField.meta, prevField.meta, options.openapiVersion),
90
- userRemoved: true,
91
- lastChangedBy: 'user'
92
- }
93
- });
94
- debug.push(`USER_REMOVED_TOMBSTONE ${id}`);
95
- continue;
96
- }
97
145
  const autoAdded = !nextField.required;
98
146
  result.push({
99
147
  ...nextField,
@@ -127,6 +175,13 @@ function mergeField(nextField, overlayField, prevField, options, debug) {
127
175
  if (prevField && prevField.type !== nextField.type) {
128
176
  merged.ui = undefined;
129
177
  merged.options = nextField.options ?? null;
178
+ merged.defaultValue = nextField.defaultValue;
179
+ merged.hidden = nextField.hidden;
180
+ merged.meta = {
181
+ ...meta,
182
+ userRemoved: false,
183
+ lastChangedBy: 'api'
184
+ };
130
185
  debug.push(`TYPE_CHANGED ${meta.id}`);
131
186
  }
132
187
  const prevEnum = Array.isArray(prevField?.options);
package/dist/index.js CHANGED
@@ -5,25 +5,62 @@ const commander_1 = require("commander");
5
5
  const generate_1 = require("./commands/generate");
6
6
  const angular_1 = require("./commands/angular");
7
7
  const login_1 = require("./commands/login");
8
+ const merge_1 = require("./commands/merge");
8
9
  const config_1 = require("./runtime/config");
9
10
  const telemetry_1 = require("./telemetry");
11
+ const logger_1 = require("./runtime/logger");
10
12
  const program = new commander_1.Command();
11
13
  program
12
14
  .name('generate-ui')
13
15
  .description('Generate UI from OpenAPI')
14
16
  .version((0, config_1.getCliVersion)())
15
- .option('--no-telemetry', 'Disable telemetry');
17
+ .option('--no-telemetry', 'Disable telemetry')
18
+ .option('--dev', 'Enable verbose logs')
19
+ .option('--verbose', 'Enable verbose logs (same as --dev)');
16
20
  /**
17
- * 1️⃣ OpenAPI → Screen schemas
21
+ * 1️⃣ Default flow: OpenAPI → Schemas → Angular
18
22
  */
19
23
  program
20
24
  .command('generate')
21
- .description('Generate screen schemas from OpenAPI')
22
- .requiredOption('-o, --openapi <path>', 'OpenAPI file')
23
- .option('--output <path>', 'Output directory for generate-ui (default: ./src/generate-ui or ./generate-ui)')
25
+ .description('Generate schemas and Angular code (default flow)')
26
+ .option('-o, --openapi <path>', 'OpenAPI file (optional if configured in generateui-config.json)')
27
+ .option('--output <path>', 'Output directory for generate-ui (optional if configured in generateui-config.json)')
28
+ .option('-f, --features <path>', 'Angular features output directory (optional if configured in generateui-config.json)')
29
+ .option('-w, --watch', 'Keep watching .screen.json files after generation')
24
30
  .option('-d, --debug', 'Explain merge decisions')
25
31
  .action(async (options) => {
26
- const { telemetry } = program.opts();
32
+ const { telemetry, dev, verbose } = program.opts();
33
+ (0, logger_1.setVerbose)(Boolean(dev || verbose));
34
+ try {
35
+ await (0, telemetry_1.trackGenerateCalled)();
36
+ await (0, generate_1.generate)({
37
+ openapi: options.openapi,
38
+ output: options.output,
39
+ debug: options.debug,
40
+ telemetryEnabled: telemetry
41
+ });
42
+ await (0, angular_1.angular)({
43
+ featuresPath: options.features,
44
+ watch: Boolean(options.watch),
45
+ telemetryEnabled: telemetry
46
+ });
47
+ }
48
+ catch (error) {
49
+ handleCliError(error);
50
+ }
51
+ });
52
+ /**
53
+ * 2️⃣ Advanced: OpenAPI → Screen schemas only
54
+ */
55
+ program
56
+ .command('schema')
57
+ .description('Advanced: generate screen schemas only')
58
+ .option('-o, --openapi <path>', 'OpenAPI file (optional if configured in generateui-config.json)')
59
+ .option('--output <path>', 'Output directory for generate-ui (optional if configured in generateui-config.json)')
60
+ .option('-d, --debug', 'Explain merge decisions')
61
+ .action(async (options) => {
62
+ const { telemetry, dev, verbose } = program.opts();
63
+ (0, logger_1.setVerbose)(Boolean(dev || verbose));
27
64
  try {
28
65
  await (0, telemetry_1.trackGenerateCalled)();
29
66
  await (0, generate_1.generate)({
@@ -38,19 +75,23 @@ program
38
75
  }
39
76
  });
40
77
  /**
41
- * 2️⃣ Screen schemas → Angular code
78
+ * 3️⃣ Advanced: Screen schemas → Angular code
42
79
  */
43
80
  program
44
81
  .command('angular')
45
- .description('Generate Angular code from screen schemas')
82
+ .description('Advanced: generate Angular code from screen schemas')
46
83
  .option('-s, --schemas <path>', 'Directory containing generate-ui (with overlays/)')
47
- .option('-f, --features <path>', 'Angular features output directory')
84
+ .option('-f, --features <path>', 'Angular features output directory (optional if configured in generateui-config.json)')
85
+ .option('-w, --watch', 'Watch .screen.json files and regenerate Angular on changes (default)')
86
+ .option('--no-watch', 'Run Angular generation once and exit')
48
87
  .action(async (options) => {
49
- const { telemetry } = program.opts();
88
+ const { telemetry, dev, verbose } = program.opts();
89
+ (0, logger_1.setVerbose)(Boolean(dev || verbose));
50
90
  try {
51
91
  await (0, angular_1.angular)({
52
92
  schemasPath: options.schemas,
53
93
  featuresPath: options.features,
94
+ watch: options.watch !== false,
54
95
  telemetryEnabled: telemetry
55
96
  });
56
97
  }
@@ -59,13 +100,14 @@ program
59
100
  }
60
101
  });
61
102
  /**
62
- * 3️⃣ Login (Dev plan)
103
+ * 4️⃣ Login (Dev plan)
63
104
  */
64
105
  program
65
106
  .command('login')
66
107
  .description('Login to unlock Dev features')
67
108
  .action(async () => {
68
- const { telemetry } = program.opts();
109
+ const { telemetry, dev, verbose } = program.opts();
110
+ (0, logger_1.setVerbose)(Boolean(dev || verbose));
69
111
  try {
70
112
  await (0, login_1.login)({ telemetryEnabled: telemetry });
71
113
  }
@@ -73,9 +115,43 @@ program
73
115
  handleCliError(error);
74
116
  }
75
117
  });
118
+ /**
119
+ * 5️⃣ Compare generated vs overrides (interactive)
120
+ */
121
+ program
122
+ .command('merge')
123
+ .description('Compare generated vs overrides with an interactive diff tool')
124
+ .requiredOption('--feature <name>', 'Feature folder or operationId')
125
+ .option('-f, --features <path>', 'Angular features output directory')
126
+ .option('--file <name>', 'File to compare: component.ts, component.html, component.scss, or all')
127
+ .option('--tool <name>', 'Diff tool (code, meld, kdiff3, bc, or any executable)')
128
+ .action(async (options) => {
129
+ const { telemetry, dev, verbose } = program.opts();
130
+ (0, logger_1.setVerbose)(Boolean(dev || verbose));
131
+ try {
132
+ await (0, merge_1.merge)({
133
+ featuresPath: options.features,
134
+ feature: options.feature,
135
+ file: options.file,
136
+ tool: options.tool
137
+ });
138
+ }
139
+ catch (error) {
140
+ handleCliError(error);
141
+ }
142
+ });
76
143
  function handleCliError(error) {
77
144
  if (error instanceof Error) {
78
145
  console.error(error.message.replace(/\\n/g, '\n'));
146
+ if ((0, logger_1.isVerbose)() && error.stack) {
147
+ console.error('');
148
+ console.error('🔎 Stack trace:');
149
+ console.error(error.stack);
150
+ }
151
+ else {
152
+ console.error('');
153
+ console.error('ℹ️ Tip: re-run with --dev to see detailed logs.');
154
+ }
79
155
  }
80
156
  else {
81
157
  console.error('Unexpected error');
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.requireFeature = requireFeature;
4
+ const config_1 = require("../runtime/config");
4
5
  function requireFeature(features, featureKey, reason) {
5
6
  if (!features[featureKey]) {
6
7
  const details = reason ? ` ${reason}` : '';
7
- throw new Error(`Requires Dev plan.${details} Execute \`generate-ui login\` to continue.`);
8
+ throw new Error(`This feature requires an active paid subscription.${details} Upgrade: ${(0, config_1.getDevPlanUrl)()}`);
8
9
  }
9
10
  }