@cosider.construction/eapp 1.0.2 → 1.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosider.construction/eapp",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Project-local CLI for Electron + Vue ERP projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,8 +28,8 @@ const AUDIT_WIDTHS = { field: 20, type: 10 };
28
28
 
29
29
  // ─── Code generators ──────────────────────────────────────────────────────────
30
30
 
31
- function buildModelJs({ module, entity, schema, fields, auditFields, pkField }) {
32
- const TABLE = toUpper(entity);
31
+ function buildModelJs({ module, entity, table, schema, fields, auditFields, pkField }) {
32
+ const TABLE = toUpper(table || entity);
33
33
  const dbAlias = schema === 'dbo' ? 'dbo' : schema;
34
34
  const importPath = module === 'shared' ? '../../../engine/db.js' : '../../engine/db.js';
35
35
 
@@ -85,7 +85,7 @@ export async function remove(${pkField}) { return model.remove(${pkField}); }
85
85
  `;
86
86
  }
87
87
 
88
- function buildIpcJson({ module, entity, schema }) {
88
+ function buildIpcJson({ module, entity, table, schema }) {
89
89
  const route = ipcRoute(module, entity);
90
90
  const mod = module === 'shared' ? 'shared' : module;
91
91
  const ent = toLower(entity);
@@ -121,9 +121,17 @@ function buildMetaJson({ module, entity, schema, fields, auditFields, pkField })
121
121
 
122
122
  // ─── Write entity files ───────────────────────────────────────────────────────
123
123
 
124
- async function writeEntity({ module, entity, schema, fields, auditFields, pkField, opts, dryRun }) {
124
+ async function writeEntity({ module, entity, table, schema, fields, auditFields, pkField, opts, dryRun }) {
125
125
  const cwd = process.cwd();
126
- const backendBase = module === 'shared'
126
+ // If user typed POLE.dbo it means entity=pole, schema=dbo show confirmation
127
+ const displayPath = `${module}/${entity}` + (parsedTable !== entity ? `:${parsedTable}` : '') + `.${schema}`;
128
+ console.log('');
129
+ console.log(chalk.bold.white(' Entity ') + chalk.cyan(displayPath));
130
+ if (parsedTable !== entity) {
131
+ console.log(chalk.dim(` table name in DB will be: ${toUpper(parsedTable)}`));
132
+ }
133
+
134
+ const backendBase = module === 'shared'
127
135
  ? path.join(cwd, 'src', 'backend', 'shared', toUpper(entity))
128
136
  : path.join(cwd, 'src', 'backend', 'modules', toUpper(module), toUpper(entity));
129
137
 
@@ -131,7 +139,7 @@ async function writeEntity({ module, entity, schema, fields, auditFields, pkFiel
131
139
 
132
140
  if (o.model) {
133
141
  await writeFile(path.join(backendBase, 'model.js'),
134
- buildModelJs({ module, entity, schema, fields, auditFields, pkField }), { dryRun });
142
+ buildModelJs({ module, entity, table: table || entity, schema, fields, auditFields, pkField }), { dryRun });
135
143
  }
136
144
  if (o.controller) {
137
145
  await writeFile(path.join(backendBase, 'controller.js'),
@@ -139,7 +147,7 @@ async function writeEntity({ module, entity, schema, fields, auditFields, pkFiel
139
147
  }
140
148
  if (o.ipc) {
141
149
  await writeFile(path.join(backendBase, 'ipc.json'),
142
- buildIpcJson({ module, entity, schema }), { dryRun });
150
+ buildIpcJson({ module, entity, table: table || entity, schema }), { dryRun });
143
151
  }
144
152
  // Always write meta so registry can read it
145
153
  await writeJson(path.join(backendBase, 'entity.meta.json'),
@@ -186,12 +194,21 @@ export async function generateEntity(args, rawArgs) {
186
194
  catch (e) { log.error(e.message); process.exit(1); }
187
195
  }
188
196
 
189
- const { module, entity, schema } = parsed;
197
+ const { module, entity, table: parsedTable, schema } = parsed;
190
198
  const pkField = `${entity}_id`;
199
+ let tableOverride = parsedTable !== entity ? parsedTable : null;
191
200
 
192
201
  // ── Check if exists ───────────────────────────────────────────────────────
193
202
  const cwd = process.cwd();
194
- const backendBase = module === 'shared'
203
+ // If user typed POLE.dbo it means entity=pole, schema=dbo show confirmation
204
+ const displayPath = `${module}/${entity}` + (parsedTable !== entity ? `:${parsedTable}` : '') + `.${schema}`;
205
+ console.log('');
206
+ console.log(chalk.bold.white(' Entity ') + chalk.cyan(displayPath));
207
+ if (parsedTable !== entity) {
208
+ console.log(chalk.dim(` table name in DB will be: ${toUpper(parsedTable)}`));
209
+ }
210
+
211
+ const backendBase = module === 'shared'
195
212
  ? path.join(cwd, 'src', 'backend', 'shared', toUpper(entity))
196
213
  : path.join(cwd, 'src', 'backend', 'modules', toUpper(module), toUpper(entity));
197
214
 
@@ -204,7 +221,7 @@ export async function generateEntity(args, rawArgs) {
204
221
  // ── --default: skip all prompts ───────────────────────────────────────────
205
222
  if (useDefault) {
206
223
  await writeEntity({
207
- module, entity, schema,
224
+ module, entity, table: parsedTable || entity, schema,
208
225
  fields: defaultFillableRows(entity),
209
226
  auditFields: DEFAULT_AUDIT_ROWS,
210
227
  pkField,
@@ -246,7 +263,7 @@ export async function generateEntity(args, rawArgs) {
246
263
  const resolvedPk = pkRow ? pkRow.field : pkField;
247
264
 
248
265
  await writeEntity({
249
- module, entity, schema,
266
+ module, entity, table: tableOverride || entity, schema,
250
267
  fields: fillableRows,
251
268
  auditFields: auditRows,
252
269
  pkField: resolvedPk,
@@ -12,11 +12,12 @@ function flattenItems(items = [], parentLabel = '') {
12
12
  const rows = [];
13
13
  for (const item of items) {
14
14
  rows.push({
15
- label: item.label || '',
16
- route: item.route || '',
17
- icon: item.icon || '',
18
- parent: parentLabel,
19
- permission: item.permission || '',
15
+ label: item.label || '',
16
+ icon: item.icon || '',
17
+ path: parentLabel,
18
+ route: item.route || '',
19
+ dot: item.dot ? 'on' : 'off',
20
+ nbr: item.nbr || '',
20
21
  });
21
22
  if (item.children?.length) {
22
23
  rows.push(...flattenItems(item.children, item.label));
@@ -28,17 +29,19 @@ function flattenItems(items = [], parentLabel = '') {
28
29
  // ─── Rebuild nested menu items from flat rows ─────────────────────────────────
29
30
 
30
31
  function nestItems(rows) {
31
- const top = rows.filter(r => !r.parent);
32
- const children = rows.filter(r => r.parent);
32
+ const top = rows.filter(r => !r.path);
33
+ const children = rows.filter(r => r.path);
33
34
 
34
35
  return top.map(r => {
35
- const item = { label: r.label, route: r.route, icon: r.icon };
36
- if (r.permission) item.permission = r.permission;
37
- const kids = children.filter(c => c.parent === r.label);
36
+ const item = { label: r.label, icon: r.icon, route: r.route };
37
+ if (r.dot === 'on') item.dot = true;
38
+ if (r.nbr) item.nbr = r.nbr;
39
+ const kids = children.filter(c => c.path === r.label);
38
40
  if (kids.length) {
39
41
  item.children = kids.map(c => ({
40
- label: c.label, route: c.route, icon: c.icon,
41
- ...(c.permission ? { permission: c.permission } : {}),
42
+ label: c.label, icon: c.icon, route: c.route,
43
+ ...(c.dot === 'on' ? { dot: true } : {}),
44
+ ...(c.nbr ? { nbr: c.nbr } : {}),
42
45
  }));
43
46
  }
44
47
  return item;
@@ -59,7 +62,7 @@ async function editViewMenu(menuFile, existing) {
59
62
  // Classic — same item table as module menu
60
63
  const existing_items = flattenItems(existing?.hMenu?.items || []);
61
64
  if (!existing_items.length) {
62
- existing_items.push({ label: 'Action', route: '', icon: '', parent: '', permission: '' });
65
+ existing_items.push({ label: 'Action', icon: '', path: '', route: '', dot: 'off', nbr: '' });
63
66
  }
64
67
  console.log('');
65
68
  const rows = await fieldsTable(
@@ -185,14 +188,14 @@ export async function generateMenu(args, rawArgs) {
185
188
  console.log(chalk.bold.white(` ${toPascal(module)} — hMenu`));
186
189
  const hRows = flattenItems(existing.hMenu?.items || []);
187
190
  if (!hRows.length) {
188
- hRows.push({ label: `${toPascal(module)} Home`, route: `/${module}`, icon: '', parent: '', permission: '' });
191
+ hRows.push({ label: `${toPascal(module)} Home`, icon: '', path: '', route: `/${module}`, dot: 'off', nbr: '' });
189
192
  }
190
193
 
191
194
  const updatedH = await fieldsTable(
192
195
  hRows,
193
- ['label', 'route', 'icon', 'parent', 'permission'],
194
- { label: 16, route: 20, icon: 6, parent: 14, permission: 20 },
195
- 'hMenu (parent = dropdown group label, empty = top level enter done)'
196
+ ['label', 'icon', 'path', 'route', 'dot', 'nbr'],
197
+ { label: 16, icon: 5, path: 14, route: 20, dot: 5, nbr: 6 },
198
+ 'hMenu (path = parent group label, dot = show dot indicator, nbr = badge)'
196
199
  );
197
200
 
198
201
  // Pass 2 — vMenu
@@ -200,14 +203,14 @@ export async function generateMenu(args, rawArgs) {
200
203
  console.log(chalk.bold.white(` ${toPascal(module)} — vMenu`));
201
204
  const vRows = flattenItems(existing.vMenu?.items || []);
202
205
  if (!vRows.length) {
203
- vRows.push({ label: toPascal(module), route: `/${module}`, icon: '', parent: '', permission: '' });
206
+ vRows.push({ label: toPascal(module), icon: '', path: '', route: `/${module}`, dot: 'off', nbr: '' });
204
207
  }
205
208
 
206
209
  const updatedV = await fieldsTable(
207
210
  vRows,
208
- ['label', 'route', 'icon', 'parent', 'permission'],
209
- { label: 16, route: 20, icon: 6, parent: 14, permission: 20 },
210
- 'vMenu (parent = collapsible group label, empty = top level enter done)'
211
+ ['label', 'icon', 'path', 'route', 'dot', 'nbr'],
212
+ { label: 16, icon: 5, path: 14, route: 20, dot: 5, nbr: 6 },
213
+ 'vMenu (path = collapsible group label, dot = show dot indicator, nbr = badge)'
211
214
  );
212
215
 
213
216
  // Also check hMenu style
@@ -121,7 +121,7 @@ export async function generateModule(args, rawArgs) {
121
121
  return {
122
122
  module: lower,
123
123
  layout: `${toPascal(lower)}Layout`,
124
- vmenu: 'own',
124
+ hmenu: 'own',
125
125
  home: 'on',
126
126
  grid: 'on',
127
127
  };
@@ -138,7 +138,7 @@ export async function generateModule(args, rawArgs) {
138
138
  newName = toLower(newName.trim());
139
139
  if (!isValidName(newName)) { log.error('Invalid module name.'); process.exit(1); }
140
140
  if (!existing.find(e => toLower(e) === newName)) {
141
- rows.push({ module: newName, layout: `${toPascal(newName)}Layout`, vmenu: 'own', home: 'on', grid: 'on' });
141
+ rows.push({ module: newName, layout: `${toPascal(newName)}Layout`, hmenu: 'own', home: 'on', grid: '3x2' });
142
142
  }
143
143
  }
144
144
 
@@ -146,17 +146,18 @@ export async function generateModule(args, rawArgs) {
146
146
  console.log('');
147
147
  const updated = await fieldsTable(
148
148
  rows,
149
- ['module', 'layout', 'vmenu', 'home', 'grid'],
150
- { module: 14, layout: 18, vmenu: 8, home: 6, grid: 6 },
151
- 'Modules (edit layout name, vmenu: own|share, home/grid: on|off enter to save)'
149
+ ['module', 'layout', 'hmenu', 'home', 'grid'],
150
+ { module: 14, layout: 18, hmenu: 8, home: 6, grid: 8 },
151
+ 'Modules (hmenu: own|share|none home: on|off grid: 0 or NxM e.g. 3x2 enter to save)'
152
152
  );
153
153
 
154
154
  // ── Write new/updated modules ──────────────────────────────────────────────
155
155
  for (const row of updated) {
156
156
  const name = toLower(row.module);
157
157
  const hasHome = row.home !== 'off';
158
- const hasGrid = row.grid !== 'off';
159
- const ownVMenu = row.vmenu !== 'share';
158
+ const gridVal = row.grid || '0'; // '0' = no grid, 'NxM' = N cols x M rows
159
+ const hasGrid = gridVal !== '0' && gridVal !== 'off';
160
+ const hMenuScope = row.hmenu || 'own'; // own | share | none
160
161
  const layoutName = row.layout || `${toPascal(name)}Layout`;
161
162
 
162
163
  const backendDir = path.join(cwd, 'src', 'backend', 'modules', toUpper(name));
@@ -0,0 +1,118 @@
1
+ import path from 'path';
2
+ import fs from 'fs-extra';
3
+ import chalk from 'chalk';
4
+ import { toUpper, toLower, toPascal } from '../utils/naming.js';
5
+ import { writeFile, assertProjectRoot, loadEntityRegistry } from '../utils/fs.js';
6
+ import { log } from '../utils/log.js';
7
+ import { radioLine, textInput } from '../tui/engine.js';
8
+
9
+ // ─── SQL generators ───────────────────────────────────────────────────────────
10
+
11
+ function sqlTypeMap(appType) {
12
+ const map = {
13
+ varchar: 'NVARCHAR(255)',
14
+ int: 'INT',
15
+ decimal: 'DECIMAL(18,2)',
16
+ date: 'DATE',
17
+ datetime: 'DATETIME2',
18
+ bit: 'BIT',
19
+ text: 'NVARCHAR(MAX)',
20
+ float: 'FLOAT',
21
+ enum: 'NVARCHAR(50)',
22
+ };
23
+ return map[appType] || 'NVARCHAR(255)';
24
+ }
25
+
26
+ function buildCreateTable({ entity, table, schema, fields, auditFields, pkField }) {
27
+ const TABLE = toUpper(table || entity);
28
+ const SCHEMA = schema || 'dbo';
29
+ const allCols = [...(fields || []), ...(auditFields || [])];
30
+
31
+ const colLines = allCols.map(f => {
32
+ const nullable = f.pk ? 'NOT NULL' : 'NULL';
33
+ const identity = f.pk ? ' IDENTITY(1,1)' : '';
34
+ return ` [${f.field}] ${sqlTypeMap(f.type)}${identity} ${nullable}`;
35
+ });
36
+
37
+ const pkLine = ` CONSTRAINT [PK_${TABLE}] PRIMARY KEY ([${pkField || `${entity}_id`}])`;
38
+
39
+ return `-- Generated by eapp — ${new Date().toISOString().slice(0, 10)}
40
+ -- Entity : ${toLower(entity)}
41
+ -- Table : [${SCHEMA}].[${TABLE}]
42
+
43
+ CREATE TABLE [${SCHEMA}].[${TABLE}] (
44
+ ${[...colLines, pkLine].join(',\n')}
45
+ );
46
+ GO
47
+ `;
48
+ }
49
+
50
+ function buildAlterTable({ entity, table, schema, fields }) {
51
+ const TABLE = toUpper(table || entity);
52
+ const SCHEMA = schema || 'dbo';
53
+
54
+ const lines = (fields || []).map(f =>
55
+ `ALTER TABLE [${SCHEMA}].[${TABLE}] ADD [${f.field}] ${sqlTypeMap(f.type)} NULL;`
56
+ );
57
+
58
+ return `-- Alter: add missing columns to [${SCHEMA}].[${TABLE}]
59
+ ${lines.join('\n')}
60
+ GO
61
+ `;
62
+ }
63
+
64
+ // ─── Main generator ───────────────────────────────────────────────────────────
65
+
66
+ export async function generateSql(args, rawArgs) {
67
+ assertProjectRoot();
68
+
69
+ const cwd = process.cwd();
70
+ const registry = await loadEntityRegistry(cwd);
71
+
72
+ if (!registry.size) {
73
+ log.warn('No entities found. Run eapp entity first.');
74
+ return;
75
+ }
76
+
77
+ // Pick entity from registry
78
+ const entries = [...registry.entries()];
79
+ console.log('');
80
+ console.log(chalk.bold.white(' Generate SQL for entity'));
81
+ const key = await textInput('module/entity (or "all"):');
82
+
83
+ const targets = key.trim() === 'all'
84
+ ? entries
85
+ : entries.filter(([k]) => k.endsWith(key.trim().toLowerCase()) || k === key.trim().toLowerCase());
86
+
87
+ if (!targets.length) {
88
+ log.error(`Entity "${key}" not found in registry.`);
89
+ return;
90
+ }
91
+
92
+ // Pick statement type
93
+ console.log('');
94
+ console.log(chalk.bold.white(' Statement type'));
95
+ const stmtType = await radioLine('', [
96
+ { label: 'CREATE TABLE', value: 'create' },
97
+ { label: 'ALTER (add cols)', value: 'alter' },
98
+ ]);
99
+
100
+ const sqlDir = path.join(cwd, 'src', 'backend', 'sql');
101
+ await fs.ensureDir(sqlDir);
102
+
103
+ for (const [, meta] of targets) {
104
+ const { module, entity, table, schema, fields, auditFields, pkField } = meta;
105
+ const fileName = stmtType === 'create'
106
+ ? `${schema}_${toUpper(table || entity)}_create.sql`
107
+ : `${schema}_${toUpper(table || entity)}_alter.sql`;
108
+ const filePath = path.join(sqlDir, fileName);
109
+ const content = stmtType === 'create'
110
+ ? buildCreateTable({ entity, table, schema, fields, auditFields, pkField })
111
+ : buildAlterTable({ entity, table, schema, fields });
112
+
113
+ await writeFile(filePath, content);
114
+ log.success(`${fileName}`);
115
+ }
116
+
117
+ log.info(`SQL files written to src/backend/sql/`);
118
+ }
@@ -349,11 +349,19 @@ export async function generateView(args, rawArgs) {
349
349
  defaultComps,
350
350
  COMP_COLS,
351
351
  COMP_WIDTHS,
352
- 'Components (+ add - remove cols space menu toggle enter done)'
352
+ 'Components (+ add - remove \u2190 \u2192 cols space menu toggle enter done)'
353
353
  );
354
354
 
355
- const hasRow = compRows.some(r => r.render === 'row' || r.render === 'table');
356
- const hasCard = compRows.some(r => r.render === 'card');
355
+ // List render mode — explicit radio (table / card / both) so user can't mistype
356
+ console.log('');
357
+ console.log(chalk.bold.white(' List render'));
358
+ const listRender = await radioLine('', [
359
+ { label: 'Table (row)', value: 'table' },
360
+ { label: 'Card', value: 'card' },
361
+ { label: 'Both', value: 'both' },
362
+ ]);
363
+ const hasRow = listRender === 'table' || listRender === 'both';
364
+ const hasCard = listRender === 'card' || listRender === 'both';
357
365
 
358
366
  // ── Step 4: slaves ────────────────────────────────────────────────────────
359
367
  let slaveConfigs = [];
package/src/index.js CHANGED
@@ -39,28 +39,74 @@ Options:
39
39
  --help, -h Show this help
40
40
  `;
41
41
 
42
+ const MAIN_MENU = [
43
+ { label: 'Entity', value: 'entity' },
44
+ { label: 'Pivot', value: 'pivot' },
45
+ { label: 'View', value: 'view' },
46
+ { label: 'Module', value: 'module' },
47
+ { label: 'Menu', value: 'menu' },
48
+ { label: 'Sql', value: 'sql' },
49
+ { label: 'Config', value: 'config' },
50
+ { label: 'Layout', value: 'layout' },
51
+ { label: 'List', value: 'list' },
52
+ { label: 'Quit', value: 'quit' },
53
+ ];
54
+
42
55
  async function launchInteractive() {
43
- console.log('');
44
- console.log(chalk.bold.cyan(' eapp') + chalk.dim(' — what do you want to do?'));
45
- console.log('');
46
- const chosen = await radioLine('', [
47
- { label: 'Entity', value: 'entity' },
48
- { label: 'View', value: 'view' },
49
- { label: 'Module', value: 'module' },
50
- { label: 'Menu', value: 'menu' },
51
- { label: 'Config', value: 'config' },
52
- { label: 'Pivot', value: 'pivot' },
53
- { label: 'Layout', value: 'layout' },
54
- { label: 'List', value: 'list' },
55
- ]);
56
- console.log('');
57
- if (chosen === 'list') {
58
- await listModules();
56
+ let lastIdx = 0;
57
+
58
+ // eslint-disable-next-line no-constant-condition
59
+ while (true) {
60
+ // Clear screen so each iteration feels fresh
61
+ process.stdout.write('\x1B[2J\x1B[H');
62
+ console.log('');
63
+ console.log(chalk.bold.cyan(' eapp') + chalk.dim(' — what do you want to do?'));
64
+ console.log(chalk.dim(' → select enter confirm Quit to exit'));
65
+
66
+ const chosen = await radioLine('', MAIN_MENU, lastIdx);
67
+ lastIdx = MAIN_MENU.findIndex(m => m.value === chosen);
68
+
59
69
  console.log('');
60
- await listEntities([]);
61
- return;
70
+
71
+ if (chosen === 'quit') {
72
+ console.log(chalk.dim(' Bye.\n'));
73
+ break;
74
+ }
75
+
76
+ if (chosen === 'sql') {
77
+ const { generateSql } = await import('./generators/sql.js');
78
+ await generateSql([], []);
79
+ } else if (chosen === 'list') {
80
+ await listModules();
81
+ console.log('');
82
+ await listEntities([]);
83
+ } else {
84
+ await run(chosen, [], []);
85
+ }
86
+
87
+ // Pause briefly so the user can read the result before the menu redraws
88
+ console.log('');
89
+ console.log(chalk.dim(' Press enter to return to the menu…'));
90
+ await waitEnter();
62
91
  }
63
- await run(chosen, [], process.argv.slice(2));
92
+ }
93
+
94
+ function waitEnter() {
95
+ return new Promise((resolve) => {
96
+ process.stdin.resume();
97
+ process.stdin.setEncoding('utf8');
98
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
99
+ const onKey = (key) => {
100
+ if (key === '\r' || key === '\n' || key === '\x03') {
101
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
102
+ process.stdin.pause();
103
+ process.stdin.removeListener('data', onKey);
104
+ if (key === '\x03') process.exit(0);
105
+ resolve();
106
+ }
107
+ };
108
+ process.stdin.on('data', onKey);
109
+ });
64
110
  }
65
111
 
66
112
  async function run(command, args, rawArgs) {
package/src/tui/engine.js CHANGED
@@ -10,42 +10,37 @@ import chalk from 'chalk';
10
10
  // ─── Raw input helpers ────────────────────────────────────────────────────────
11
11
 
12
12
  export function enableRawInput() {
13
- if (process.stdin.isTTY) {
14
- process.stdin.setRawMode(true);
15
- }
13
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
16
14
  process.stdin.resume();
17
15
  process.stdin.setEncoding('utf8');
18
16
  }
19
17
 
20
18
  export function disableRawInput() {
21
- if (process.stdin.isTTY) {
22
- process.stdin.setRawMode(false);
23
- }
19
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
24
20
  process.stdin.pause();
25
21
  }
26
22
 
27
23
  export function clearLines(n) {
28
- for (let i = 0; i < n; i++) {
29
- process.stdout.write('\x1B[1A\x1B[2K');
30
- }
24
+ for (let i = 0; i < n; i++) process.stdout.write('\x1B[1A\x1B[2K');
31
25
  }
32
26
 
33
- export function hideCursor() { process.stdout.write('\x1B[?25l'); }
34
- export function showCursor() { process.stdout.write('\x1B[?25h'); }
27
+ export function clearScreen() { process.stdout.write('\x1B[2J\x1B[H'); }
28
+ export function hideCursor() { process.stdout.write('\x1B[?25l'); }
29
+ export function showCursor() { process.stdout.write('\x1B[?25h'); }
35
30
 
36
31
  // ─── Key constants ────────────────────────────────────────────────────────────
37
32
 
38
33
  export const KEYS = {
39
- UP: '\x1B[A',
40
- DOWN: '\x1B[B',
41
- RIGHT: '\x1B[C',
42
- LEFT: '\x1B[D',
43
- ENTER: '\r',
44
- SPACE: ' ',
45
- PLUS: '+',
46
- MINUS: '-',
47
- ESC: '\x1B',
48
- CTRL_C: '\x03',
34
+ UP: '\x1B[A',
35
+ DOWN: '\x1B[B',
36
+ RIGHT: '\x1B[C',
37
+ LEFT: '\x1B[D',
38
+ ENTER: '\r',
39
+ SPACE: ' ',
40
+ PLUS: '+',
41
+ MINUS: '-',
42
+ ESC: '\x1B',
43
+ CTRL_C: '\x03',
49
44
  BACKSPACE: '\x7F',
50
45
  };
51
46
 
@@ -65,8 +60,8 @@ export async function textInput(prompt, defaultVal = '') {
65
60
  // ─── Radio selector ───────────────────────────────────────────────────────────
66
61
 
67
62
  /**
68
- * Horizontal radio — one choice from options
69
- * Returns selected value
63
+ * Horizontal radio — one choice from options.
64
+ * Returns selected value.
70
65
  */
71
66
  export async function radioLine(prompt, options, defaultIdx = 0) {
72
67
  let selected = defaultIdx;
@@ -75,9 +70,7 @@ export async function radioLine(prompt, options, defaultIdx = 0) {
75
70
  process.stdout.clearLine?.(0);
76
71
  process.stdout.cursorTo?.(0);
77
72
  const opts = options.map((o, i) =>
78
- i === selected
79
- ? chalk.green(`◉ ${o.label}`)
80
- : chalk.dim(`○ ${o.label}`)
73
+ i === selected ? chalk.green(`◉ ${o.label}`) : chalk.dim(`○ ${o.label}`)
81
74
  ).join(' ');
82
75
  process.stdout.write(` ${chalk.cyan(prompt)} ${opts}`);
83
76
  };
@@ -90,9 +83,9 @@ export async function radioLine(prompt, options, defaultIdx = 0) {
90
83
 
91
84
  const onKey = (key) => {
92
85
  if (key === KEYS.CTRL_C) { showCursor(); disableRawInput(); process.exit(0); }
93
- if (key === KEYS.LEFT) { selected = (selected - 1 + options.length) % options.length; render(); }
94
- if (key === KEYS.RIGHT) { selected = (selected + 1) % options.length; render(); }
95
- if (key === KEYS.ENTER) {
86
+ if (key === KEYS.LEFT) { selected = (selected - 1 + options.length) % options.length; render(); }
87
+ if (key === KEYS.RIGHT) { selected = (selected + 1) % options.length; render(); }
88
+ if (key === KEYS.ENTER) {
96
89
  process.stdout.write('\n');
97
90
  process.stdin.removeListener('data', onKey);
98
91
  showCursor();
@@ -107,43 +100,46 @@ export async function radioLine(prompt, options, defaultIdx = 0) {
107
100
  // ─── Options toggles ──────────────────────────────────────────────────────────
108
101
 
109
102
  /**
110
- * One-line toggleable options
103
+ * One-line toggleable options.
111
104
  * Returns object { [key]: boolean }
112
105
  */
113
106
  export async function optionsLine(options) {
114
- // options = [{ key, label, default }]
115
107
  const state = {};
116
108
  options.forEach(o => { state[o.key] = o.default !== false; });
117
109
  let cursor = 0;
118
110
 
119
- const render = (lines = 1) => {
120
- clearLines(lines);
121
- const rendered = options.map((o, i) => {
122
- const on = state[o.key];
123
- const box = on ? chalk.green('◼') : chalk.dim('◻');
124
- const label = i === cursor ? chalk.bold.white(o.label) : (on ? chalk.white(o.label) : chalk.dim(o.label));
111
+ // Lines: 1 (options row) + 1 (hint row) always exactly 2
112
+ const RENDERED_LINES = 2;
113
+
114
+ const renderRow = () => {
115
+ return options.map((o, i) => {
116
+ const on = state[o.key];
117
+ const box = on ? chalk.green('◼') : chalk.dim('◻');
118
+ const label = i === cursor
119
+ ? chalk.bold.white(o.label)
120
+ : (on ? chalk.white(o.label) : chalk.dim(o.label));
125
121
  return `${box} ${label}`;
126
122
  }).join(' ');
127
- console.log(' ' + rendered);
128
- console.log(chalk.dim(' ← → navigate space toggle enter confirm'));
129
123
  };
130
124
 
131
125
  return new Promise((resolve) => {
132
126
  hideCursor();
133
127
  enableRawInput();
134
128
  console.log('');
135
- const rendered = options.map((o) => {
136
- const on = state[o.key];
137
- return `${on ? chalk.green('◼') : chalk.dim('◻')} ${on ? chalk.white(o.label) : chalk.dim(o.label)}`;
138
- }).join(' ');
139
- console.log(' ' + rendered);
129
+ console.log(' ' + renderRow());
140
130
  console.log(chalk.dim(' ← → navigate space toggle enter confirm'));
141
131
 
142
132
  const onKey = (key) => {
143
133
  if (key === KEYS.CTRL_C) { showCursor(); disableRawInput(); process.exit(0); }
144
- if (key === KEYS.LEFT) { cursor = (cursor - 1 + options.length) % options.length; render(2); }
145
- if (key === KEYS.RIGHT) { cursor = (cursor + 1) % options.length; render(2); }
146
- if (key === KEYS.SPACE) { state[options[cursor].key] = !state[options[cursor].key]; render(2); }
134
+ if (key === KEYS.LEFT) { cursor = (cursor - 1 + options.length) % options.length; }
135
+ if (key === KEYS.RIGHT) { cursor = (cursor + 1) % options.length; }
136
+ if (key === KEYS.SPACE) { state[options[cursor].key] = !state[options[cursor].key]; }
137
+ if (key === KEYS.LEFT || key === KEYS.RIGHT || key === KEYS.SPACE) {
138
+ clearLines(RENDERED_LINES);
139
+ console.log(' ' + renderRow());
140
+ console.log(chalk.dim(' ← → navigate space toggle enter confirm'));
141
+ return;
142
+ }
147
143
  if (key === KEYS.ENTER) {
148
144
  process.stdin.removeListener('data', onKey);
149
145
  showCursor();
@@ -157,72 +153,108 @@ export async function optionsLine(options) {
157
153
 
158
154
  // ─── Interactive table ────────────────────────────────────────────────────────
159
155
 
160
- const COL_WIDTHS = {
161
- field: 18,
162
- type: 10,
163
- values: 14,
164
- pk: 4,
165
- fk: 4,
166
- group: 14,
167
- };
168
-
169
156
  const FIELD_TYPES = ['varchar', 'int', 'decimal', 'date', 'datetime', 'bit', 'text', 'float', 'enum'];
170
157
 
171
- const AUDIT_COL_WIDTHS = { field: 20, type: 10 };
158
+ // Columns where space toggles a boolean (rendered as ◼/◻ checkboxes)
159
+ const BOOL_COLS = new Set(['pk', 'fk']);
160
+
161
+ // Columns where space cycles between string values
162
+ const CYCLE_COLS = {
163
+ vmenu: ['own', 'share'],
164
+ hmenu: ['own', 'share', 'none'],
165
+ home: ['on', 'off'],
166
+ // 'grid' is free-text (NxM), not cycled
167
+ };
172
168
 
169
+ // Strip ANSI escape codes to get the true printable length of a string.
170
+ const ANSI_RE = /\x1B\[[0-9;]*m/g;
171
+ function visibleLen(s) { return String(s).replace(ANSI_RE, '').length; }
172
+
173
+ /**
174
+ * Pad or truncate str to exactly len visible characters.
175
+ * Too-long strings are cut with a trailing ellipsis so column borders stay aligned.
176
+ */
173
177
  function pad(str, len) {
174
- const s = String(str ?? '');
175
- return s.length >= len ? s.slice(0, len - 1) + ' ' : s + ' '.repeat(len - s.length);
178
+ const s = String(str ?? '');
179
+ const vl = visibleLen(s);
180
+ if (vl === len) return s;
181
+ if (vl > len) return s.replace(ANSI_RE, '').slice(0, len - 1) + '…';
182
+ return s + ' '.repeat(len - vl);
176
183
  }
177
184
 
178
185
  function renderTableHeader(cols, widths) {
179
- const header = cols.map(c => chalk.bold.cyan(pad(c, widths[c] || 12))).join(chalk.dim('│'));
186
+ const header = cols.map(c => chalk.bold.cyan(pad(c, widths[c] || 12))).join(chalk.dim('│'));
180
187
  const divider = cols.map(c => '─'.repeat(widths[c] || 12)).join('┼');
181
188
  return ` ┌${'─'.repeat(divider.length)}┐\n │${header}│\n ├${divider}┤`;
182
189
  }
183
190
 
184
191
  function renderFieldRow(row, cols, widths, isActive, activeCol) {
185
192
  const cells = cols.map((col, ci) => {
186
- let val = row[col] ?? '';
187
- if (col === 'pk' || col === 'fk') val = val ? chalk.green('◼') : chalk.dim('◻');
193
+ const colW = widths[col] || 12;
188
194
  const isThisCell = isActive && ci === activeCol;
189
- const cell = pad(val, widths[col] || 12);
190
- return isThisCell ? chalk.bgBlue.white(cell) : (isActive ? chalk.white(cell) : chalk.dim(cell));
195
+
196
+ let cell;
197
+ if (BOOL_COLS.has(col)) {
198
+ // Boolean: pad the plain glyph first so width is exact, then colorize
199
+ const glyph = pad(row[col] ? '◼' : '◻', colW);
200
+ cell = isThisCell
201
+ ? chalk.bgBlue.white(glyph)
202
+ : (row[col] ? chalk.green(glyph) : chalk.dim(glyph));
203
+ } else {
204
+ // Text / cycle: pad plain text, then colorize — ANSI codes added after padding
205
+ const plain = pad(String(row[col] ?? ''), colW);
206
+ cell = isThisCell
207
+ ? chalk.bgBlue.white(plain)
208
+ : (isActive ? chalk.white(plain) : chalk.dim(plain));
209
+ }
210
+
211
+ return cell;
191
212
  }).join(chalk.dim('│'));
192
213
  return ` │${cells}│`;
193
214
  }
194
215
 
195
216
  /**
196
- * Interactive fields table
197
- * Returns array of field objects
217
+ * Count of lines the table currently occupies (for accurate clearLines).
218
+ * 1 blank line before title
219
+ * 1 title line
220
+ * 3 header block (top border + header row + divider)
221
+ * N data rows
222
+ * 1 bottom border
223
+ * 1 hint line
224
+ * + type menu lines (when open)
225
+ */
226
+ function tableLineCount(rowCount, typeMenuOpen) {
227
+ return 1 + 1 + 3 + rowCount + 1 + 1 + (typeMenuOpen ? FIELD_TYPES.length + 2 : 0);
228
+ }
229
+
230
+ function cycleValue(col, current) {
231
+ const opts = CYCLE_COLS[col];
232
+ if (!opts) return current;
233
+ return opts[(opts.indexOf(current) + 1) % opts.length];
234
+ }
235
+
236
+ /**
237
+ * Interactive fields table.
238
+ * Returns array of field objects.
198
239
  */
199
240
  export async function fieldsTable(initialRows, cols, widths, title) {
200
241
  const rows = initialRows.map(r => ({ ...r }));
201
242
  let rowIdx = 0;
202
243
  let colIdx = 0;
203
- let editBuffer = null; // currently editing text cell
204
244
  let typeMenuOpen = false;
205
- let typeMenuIdx = 0;
206
-
207
- const toggleCols = cols.filter(c => c === 'pk' || c === 'fk');
208
- const textCols = cols.filter(c => c !== 'pk' && c !== 'fk');
209
-
210
- function totalLines() {
211
- return rows.length + 4; // header(3) + footer(1)
212
- }
245
+ let typeMenuIdx = 0;
213
246
 
214
247
  function render(initial = false) {
215
- if (!initial) clearLines(totalLines() + (typeMenuOpen ? FIELD_TYPES.length + 2 : 0));
248
+ if (!initial) clearLines(tableLineCount(rows.length, typeMenuOpen));
216
249
 
217
250
  console.log('\n ' + chalk.bold.white(title));
218
251
  console.log(renderTableHeader(cols, widths));
219
-
220
252
  rows.forEach((row, ri) => {
221
253
  console.log(renderFieldRow(row, cols, widths, ri === rowIdx, colIdx));
222
254
  });
223
-
224
- console.log(` └${'─'.repeat(cols.map(c => widths[c] || 12).reduce((a, b) => a + b, 0) + cols.length - 1)}┘`);
225
- console.log(chalk.dim(' ← → cols ↑ ↓ rows + add row - del row space toggle pk/fk enter done'));
255
+ const totalW = cols.map(c => widths[c] || 12).reduce((a, b) => a + b + 1, -1);
256
+ console.log(` └${'─'.repeat(totalW)}┘`);
257
+ console.log(chalk.dim(' ← → cols ↑ ↓ rows + add - del space toggle/cycle enter done'));
226
258
 
227
259
  if (typeMenuOpen) {
228
260
  console.log('');
@@ -240,30 +272,29 @@ export async function fieldsTable(initialRows, cols, widths, title) {
240
272
  const onKey = (key) => {
241
273
  if (key === KEYS.CTRL_C) { showCursor(); disableRawInput(); process.exit(0); }
242
274
 
243
- // Type menu open
275
+ // ── Type picker ─────────────────────────────────────────────────────────
244
276
  if (typeMenuOpen) {
245
277
  if (key === KEYS.UP) { typeMenuIdx = (typeMenuIdx - 1 + FIELD_TYPES.length) % FIELD_TYPES.length; render(); return; }
246
278
  if (key === KEYS.DOWN) { typeMenuIdx = (typeMenuIdx + 1) % FIELD_TYPES.length; render(); return; }
247
- if (key === KEYS.ENTER) {
248
- rows[rowIdx].type = FIELD_TYPES[typeMenuIdx];
249
- typeMenuOpen = false;
250
- render();
251
- return;
252
- }
253
- if (key === KEYS.ESC) { typeMenuOpen = false; render(); return; }
279
+ if (key === KEYS.ENTER) { rows[rowIdx].type = FIELD_TYPES[typeMenuIdx]; typeMenuOpen = false; render(); return; }
280
+ if (key === KEYS.ESC) { typeMenuOpen = false; render(); return; }
254
281
  return;
255
282
  }
256
283
 
257
- // Navigation
258
- if (key === KEYS.UP) { rowIdx = Math.max(0, rowIdx - 1); editBuffer = null; render(); return; }
259
- if (key === KEYS.DOWN) { rowIdx = Math.min(rows.length - 1, rowIdx + 1); editBuffer = null; render(); return; }
260
- if (key === KEYS.LEFT) { colIdx = Math.max(0, colIdx - 1); editBuffer = null; render(); return; }
261
- if (key === KEYS.RIGHT) { colIdx = Math.min(cols.length - 1, colIdx + 1); editBuffer = null; render(); return; }
284
+ // ── Navigation ──────────────────────────────────────────────────────────
285
+ if (key === KEYS.UP) { rowIdx = Math.max(0, rowIdx - 1); render(); return; }
286
+ if (key === KEYS.DOWN) { rowIdx = Math.min(rows.length - 1, rowIdx + 1); render(); return; }
287
+ if (key === KEYS.LEFT) { colIdx = Math.max(0, colIdx - 1); render(); return; }
288
+ if (key === KEYS.RIGHT) { colIdx = Math.min(cols.length - 1, colIdx + 1); render(); return; }
262
289
 
263
- // Add / remove rows
290
+ // ── Add / remove rows ───────────────────────────────────────────────────
264
291
  if (key === KEYS.PLUS) {
265
292
  const newRow = {};
266
- cols.forEach(c => { newRow[c] = c === 'pk' || c === 'fk' ? false : ''; });
293
+ cols.forEach(c => {
294
+ if (BOOL_COLS.has(c)) newRow[c] = false;
295
+ else if (CYCLE_COLS[c]) newRow[c] = CYCLE_COLS[c][0];
296
+ else newRow[c] = '';
297
+ });
267
298
  rows.splice(rowIdx + 1, 0, newRow);
268
299
  rowIdx++;
269
300
  render();
@@ -275,29 +306,35 @@ export async function fieldsTable(initialRows, cols, widths, title) {
275
306
  return;
276
307
  }
277
308
 
278
- // Toggle pk/fk
309
+ // ── Space ───────────────────────────────────────────────────────────────
279
310
  if (key === KEYS.SPACE) {
280
311
  const col = cols[colIdx];
281
- if (col === 'pk' || col === 'fk') {
312
+
313
+ if (BOOL_COLS.has(col)) {
282
314
  rows[rowIdx][col] = !rows[rowIdx][col];
283
- // Only one PK allowed
315
+ // Enforce single PK
284
316
  if (col === 'pk' && rows[rowIdx][col]) {
285
317
  rows.forEach((r, i) => { if (i !== rowIdx) r.pk = false; });
286
318
  }
287
319
  render();
288
320
  return;
289
321
  }
290
- // Open type menu on type column
322
+
323
+ if (CYCLE_COLS[col]) {
324
+ rows[rowIdx][col] = cycleValue(col, rows[rowIdx][col]);
325
+ render();
326
+ return;
327
+ }
328
+
291
329
  if (col === 'type') {
292
330
  typeMenuOpen = true;
293
- typeMenuIdx = FIELD_TYPES.indexOf(rows[rowIdx].type || 'varchar');
294
- if (typeMenuIdx < 0) typeMenuIdx = 0;
331
+ typeMenuIdx = Math.max(0, FIELD_TYPES.indexOf(rows[rowIdx].type || 'varchar'));
295
332
  render();
296
333
  return;
297
334
  }
298
335
  }
299
336
 
300
- // Done
337
+ // ── Enter ────────────────────────────────────────────────────────────────
301
338
  if (key === KEYS.ENTER) {
302
339
  const col = cols[colIdx];
303
340
  if (col === 'type') { typeMenuOpen = true; typeMenuIdx = 0; render(); return; }
@@ -308,9 +345,9 @@ export async function fieldsTable(initialRows, cols, widths, title) {
308
345
  return;
309
346
  }
310
347
 
311
- // Text editing for current cell
348
+ // ── Text editing ─────────────────────────────────────────────────────────
312
349
  const col = cols[colIdx];
313
- if (col === 'pk' || col === 'fk') return;
350
+ if (BOOL_COLS.has(col) || CYCLE_COLS[col]) return; // not typed
314
351
 
315
352
  if (key === KEYS.BACKSPACE) {
316
353
  rows[rowIdx][col] = String(rows[rowIdx][col] || '').slice(0, -1);
@@ -318,7 +355,6 @@ export async function fieldsTable(initialRows, cols, widths, title) {
318
355
  return;
319
356
  }
320
357
 
321
- // Printable character
322
358
  if (key.length === 1 && key >= ' ') {
323
359
  rows[rowIdx][col] = (rows[rowIdx][col] || '') + key;
324
360
  render();
@@ -27,15 +27,25 @@ export function parseEntityPath(input) {
27
27
 
28
28
  let raw = input.trim();
29
29
 
30
- // Handle old dot notation for backwards compat: rh.employee rh/employee
30
+ // Handle dot notation ONLY when the part before the dot could be a module name
31
+ // and the part after is NOT a schema keyword (dbo, etc.) — i.e. "rh.employee" → "rh/employee"
32
+ // BUT "POLE.dbo" must NOT be rewritten — POLE is the entity, dbo is the schema.
33
+ // Rule: rewrite "x.y" only when x contains no uppercase and y is not a known schema.
34
+ const SCHEMA_NAMES = new Set(['dbo', 'dbo2', 'hr', 'rh', 'stk', 'fin', 'crm', 'adm', 'sec', 'shared']);
31
35
  if (!raw.includes('/') && raw.includes('.') && raw.split('.').length === 2) {
32
- const [mod, ent] = raw.split('.');
33
- raw = `${mod}/${ent}`;
36
+ const [left, right] = raw.split('.');
37
+ const looksLikeSchema = SCHEMA_NAMES.has(right.toLowerCase()) || /^[a-z]+$/.test(right);
38
+ const looksLikeModule = /^[a-z][a-z0-9_-]*$/.test(left);
39
+ // Only rewrite if left looks like a lowercase module AND right does NOT look like a schema
40
+ if (looksLikeModule && !looksLikeSchema) {
41
+ raw = `${left}/${right}`;
42
+ }
34
43
  }
35
44
 
36
45
  // No slash at all → shared
37
46
  if (!raw.includes('/')) {
38
- return { module: 'shared', entity: toLower(raw), schema: 'dbo' };
47
+ const _e = toLower(raw);
48
+ return { module: 'shared', entity: _e, table: _e, schema: 'dbo' };
39
49
  }
40
50
 
41
51
  // Split on /
@@ -49,20 +59,34 @@ export function parseEntityPath(input) {
49
59
  const dotIdx = rest.indexOf('.');
50
60
  let entity, schema;
51
61
 
52
- if (dotIdx === -1) {
53
- // No dot schema = module (or dbo for shared)
54
- entity = toLower(rest);
62
+ // Also parse optional table alias: entity:tableName
63
+ // e.g. rh/agent:agentx.dbo → entity=agent, table=agentx, schema=dbo
64
+ let tableAlias = null;
65
+ if (rest.includes(':')) {
66
+ const colonIdx = rest.indexOf(':');
67
+ tableAlias = rest.slice(colonIdx + 1).split('.')[0]; // strip schema part
68
+ // rewrite rest without the alias for further parsing
69
+ // keep the .schema part after the alias if any
70
+ const afterColon = rest.slice(colonIdx + 1);
71
+ const schemaInAlias = afterColon.includes('.') ? afterColon.slice(afterColon.indexOf('.')) : '';
72
+ // rest becomes "entity.schema"
73
+ // rest = rest.slice(0, colonIdx) + schemaInAlias; — handled below
74
+ }
75
+ const cleanRest = rest.replace(/:([^.]+)/, ''); // strip :alias for entity/schema parsing
76
+
77
+ const dotIdx2 = cleanRest.indexOf('.');
78
+ let entity, schema;
79
+ if (dotIdx2 === -1) {
80
+ entity = toLower(cleanRest);
55
81
  schema = module === 'shared' ? 'dbo' : module;
56
82
  } else {
57
- entity = toLower(rest.slice(0, dotIdx));
58
- const schemaPart = rest.slice(dotIdx + 1);
59
- if (schemaPart === '') {
60
- // Trailing dot → schema = module
61
- schema = module === 'shared' ? 'dbo' : module;
62
- } else {
63
- schema = toLower(schemaPart);
64
- }
83
+ entity = toLower(cleanRest.slice(0, dotIdx2));
84
+ const schemaPart = cleanRest.slice(dotIdx2 + 1);
85
+ schema = schemaPart === '' ? (module === 'shared' ? 'dbo' : module) : toLower(schemaPart);
65
86
  }
87
+ const table = tableAlias ? toLower(tableAlias) : entity;
88
+ // (keep legacy variable for code below that uses dotIdx)
89
+ const dotIdx = dotIdx2;
66
90
 
67
91
  if (!isValidName(entity)) {
68
92
  throw new Error(`Invalid entity name "${entity}". Use alphanumeric, hyphens, underscores only.`);
@@ -71,7 +95,7 @@ export function parseEntityPath(input) {
71
95
  throw new Error(`Invalid module name "${module}".`);
72
96
  }
73
97
 
74
- return { module, entity, schema };
98
+ return { module, entity, table: table || entity, schema };
75
99
  }
76
100
 
77
101
  /**