@cosider.construction/eapp 1.0.1 → 1.0.3

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.1",
3
+ "version": "1.0.3",
4
4
  "description": "Project-local CLI for Electron + Vue ERP projects",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -39,28 +39,70 @@ Options:
39
39
  --help, -h Show this help
40
40
  `;
41
41
 
42
+ const MAIN_MENU = [
43
+ { label: 'Entity', value: 'entity' },
44
+ { label: 'View', value: 'view' },
45
+ { label: 'Module', value: 'module' },
46
+ { label: 'Menu', value: 'menu' },
47
+ { label: 'Config', value: 'config' },
48
+ { label: 'Pivot', value: 'pivot' },
49
+ { label: 'Layout', value: 'layout' },
50
+ { label: 'List', value: 'list' },
51
+ { label: 'Quit', value: 'quit' },
52
+ ];
53
+
42
54
  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();
55
+ let lastIdx = 0;
56
+
57
+ // eslint-disable-next-line no-constant-condition
58
+ while (true) {
59
+ // Clear screen so each iteration feels fresh
60
+ process.stdout.write('\x1B[2J\x1B[H');
61
+ console.log('');
62
+ console.log(chalk.bold.cyan(' eapp') + chalk.dim(' — what do you want to do?'));
63
+ console.log(chalk.dim(' → select enter confirm Quit to exit'));
64
+
65
+ const chosen = await radioLine('', MAIN_MENU, lastIdx);
66
+ lastIdx = MAIN_MENU.findIndex(m => m.value === chosen);
67
+
59
68
  console.log('');
60
- await listEntities([]);
61
- return;
69
+
70
+ if (chosen === 'quit') {
71
+ console.log(chalk.dim(' Bye.\n'));
72
+ break;
73
+ }
74
+
75
+ if (chosen === 'list') {
76
+ await listModules();
77
+ console.log('');
78
+ await listEntities([]);
79
+ } else {
80
+ await run(chosen, [], []);
81
+ }
82
+
83
+ // Pause briefly so the user can read the result before the menu redraws
84
+ console.log('');
85
+ console.log(chalk.dim(' Press enter to return to the menu…'));
86
+ await waitEnter();
62
87
  }
63
- await run(chosen, [], process.argv.slice(2));
88
+ }
89
+
90
+ function waitEnter() {
91
+ return new Promise((resolve) => {
92
+ process.stdin.resume();
93
+ process.stdin.setEncoding('utf8');
94
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
95
+ const onKey = (key) => {
96
+ if (key === '\r' || key === '\n' || key === '\x03') {
97
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
98
+ process.stdin.pause();
99
+ process.stdin.removeListener('data', onKey);
100
+ if (key === '\x03') process.exit(0);
101
+ resolve();
102
+ }
103
+ };
104
+ process.stdin.on('data', onKey);
105
+ });
64
106
  }
65
107
 
66
108
  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,107 @@ 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
+ home: ['on', 'off'],
165
+ grid: ['on', 'off'],
166
+ };
172
167
 
168
+ // Strip ANSI escape codes to get the true printable length of a string.
169
+ const ANSI_RE = /\x1B\[[0-9;]*m/g;
170
+ function visibleLen(s) { return String(s).replace(ANSI_RE, '').length; }
171
+
172
+ /**
173
+ * Pad or truncate str to exactly len visible characters.
174
+ * Too-long strings are cut with a trailing ellipsis so column borders stay aligned.
175
+ */
173
176
  function pad(str, len) {
174
- const s = String(str ?? '');
175
- return s.length >= len ? s.slice(0, len - 1) + ' ' : s + ' '.repeat(len - s.length);
177
+ const s = String(str ?? '');
178
+ const vl = visibleLen(s);
179
+ if (vl === len) return s;
180
+ if (vl > len) return s.replace(ANSI_RE, '').slice(0, len - 1) + '…';
181
+ return s + ' '.repeat(len - vl);
176
182
  }
177
183
 
178
184
  function renderTableHeader(cols, widths) {
179
- const header = cols.map(c => chalk.bold.cyan(pad(c, widths[c] || 12))).join(chalk.dim('│'));
185
+ const header = cols.map(c => chalk.bold.cyan(pad(c, widths[c] || 12))).join(chalk.dim('│'));
180
186
  const divider = cols.map(c => '─'.repeat(widths[c] || 12)).join('┼');
181
187
  return ` ┌${'─'.repeat(divider.length)}┐\n │${header}│\n ├${divider}┤`;
182
188
  }
183
189
 
184
190
  function renderFieldRow(row, cols, widths, isActive, activeCol) {
185
191
  const cells = cols.map((col, ci) => {
186
- let val = row[col] ?? '';
187
- if (col === 'pk' || col === 'fk') val = val ? chalk.green('◼') : chalk.dim('◻');
192
+ const colW = widths[col] || 12;
188
193
  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));
194
+
195
+ let cell;
196
+ if (BOOL_COLS.has(col)) {
197
+ // Boolean: pad the plain glyph first so width is exact, then colorize
198
+ const glyph = pad(row[col] ? '◼' : '◻', colW);
199
+ cell = isThisCell
200
+ ? chalk.bgBlue.white(glyph)
201
+ : (row[col] ? chalk.green(glyph) : chalk.dim(glyph));
202
+ } else {
203
+ // Text / cycle: pad plain text, then colorize — ANSI codes added after padding
204
+ const plain = pad(String(row[col] ?? ''), colW);
205
+ cell = isThisCell
206
+ ? chalk.bgBlue.white(plain)
207
+ : (isActive ? chalk.white(plain) : chalk.dim(plain));
208
+ }
209
+
210
+ return cell;
191
211
  }).join(chalk.dim('│'));
192
212
  return ` │${cells}│`;
193
213
  }
194
214
 
195
215
  /**
196
- * Interactive fields table
197
- * Returns array of field objects
216
+ * Count of lines the table currently occupies (for accurate clearLines).
217
+ * 1 blank line before title
218
+ * 1 title line
219
+ * 3 header block (top border + header row + divider)
220
+ * N data rows
221
+ * 1 bottom border
222
+ * 1 hint line
223
+ * + type menu lines (when open)
224
+ */
225
+ function tableLineCount(rowCount, typeMenuOpen) {
226
+ return 1 + 1 + 3 + rowCount + 1 + 1 + (typeMenuOpen ? FIELD_TYPES.length + 2 : 0);
227
+ }
228
+
229
+ function cycleValue(col, current) {
230
+ const opts = CYCLE_COLS[col];
231
+ if (!opts) return current;
232
+ return opts[(opts.indexOf(current) + 1) % opts.length];
233
+ }
234
+
235
+ /**
236
+ * Interactive fields table.
237
+ * Returns array of field objects.
198
238
  */
199
239
  export async function fieldsTable(initialRows, cols, widths, title) {
200
240
  const rows = initialRows.map(r => ({ ...r }));
201
241
  let rowIdx = 0;
202
242
  let colIdx = 0;
203
- let editBuffer = null; // currently editing text cell
204
243
  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
- }
244
+ let typeMenuIdx = 0;
213
245
 
214
246
  function render(initial = false) {
215
- if (!initial) clearLines(totalLines() + (typeMenuOpen ? FIELD_TYPES.length + 2 : 0));
247
+ if (!initial) clearLines(tableLineCount(rows.length, typeMenuOpen));
216
248
 
217
249
  console.log('\n ' + chalk.bold.white(title));
218
250
  console.log(renderTableHeader(cols, widths));
219
-
220
251
  rows.forEach((row, ri) => {
221
252
  console.log(renderFieldRow(row, cols, widths, ri === rowIdx, colIdx));
222
253
  });
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'));
254
+ const totalW = cols.map(c => widths[c] || 12).reduce((a, b) => a + b + 1, -1);
255
+ console.log(` └${'─'.repeat(totalW)}┘`);
256
+ console.log(chalk.dim(' ← → cols ↑ ↓ rows + add - del space toggle/cycle enter done'));
226
257
 
227
258
  if (typeMenuOpen) {
228
259
  console.log('');
@@ -240,30 +271,29 @@ export async function fieldsTable(initialRows, cols, widths, title) {
240
271
  const onKey = (key) => {
241
272
  if (key === KEYS.CTRL_C) { showCursor(); disableRawInput(); process.exit(0); }
242
273
 
243
- // Type menu open
274
+ // ── Type picker ─────────────────────────────────────────────────────────
244
275
  if (typeMenuOpen) {
245
276
  if (key === KEYS.UP) { typeMenuIdx = (typeMenuIdx - 1 + FIELD_TYPES.length) % FIELD_TYPES.length; render(); return; }
246
277
  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; }
278
+ if (key === KEYS.ENTER) { rows[rowIdx].type = FIELD_TYPES[typeMenuIdx]; typeMenuOpen = false; render(); return; }
279
+ if (key === KEYS.ESC) { typeMenuOpen = false; render(); return; }
254
280
  return;
255
281
  }
256
282
 
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; }
283
+ // ── Navigation ──────────────────────────────────────────────────────────
284
+ if (key === KEYS.UP) { rowIdx = Math.max(0, rowIdx - 1); render(); return; }
285
+ if (key === KEYS.DOWN) { rowIdx = Math.min(rows.length - 1, rowIdx + 1); render(); return; }
286
+ if (key === KEYS.LEFT) { colIdx = Math.max(0, colIdx - 1); render(); return; }
287
+ if (key === KEYS.RIGHT) { colIdx = Math.min(cols.length - 1, colIdx + 1); render(); return; }
262
288
 
263
- // Add / remove rows
289
+ // ── Add / remove rows ───────────────────────────────────────────────────
264
290
  if (key === KEYS.PLUS) {
265
291
  const newRow = {};
266
- cols.forEach(c => { newRow[c] = c === 'pk' || c === 'fk' ? false : ''; });
292
+ cols.forEach(c => {
293
+ if (BOOL_COLS.has(c)) newRow[c] = false;
294
+ else if (CYCLE_COLS[c]) newRow[c] = CYCLE_COLS[c][0];
295
+ else newRow[c] = '';
296
+ });
267
297
  rows.splice(rowIdx + 1, 0, newRow);
268
298
  rowIdx++;
269
299
  render();
@@ -275,29 +305,35 @@ export async function fieldsTable(initialRows, cols, widths, title) {
275
305
  return;
276
306
  }
277
307
 
278
- // Toggle pk/fk
308
+ // ── Space ───────────────────────────────────────────────────────────────
279
309
  if (key === KEYS.SPACE) {
280
310
  const col = cols[colIdx];
281
- if (col === 'pk' || col === 'fk') {
311
+
312
+ if (BOOL_COLS.has(col)) {
282
313
  rows[rowIdx][col] = !rows[rowIdx][col];
283
- // Only one PK allowed
314
+ // Enforce single PK
284
315
  if (col === 'pk' && rows[rowIdx][col]) {
285
316
  rows.forEach((r, i) => { if (i !== rowIdx) r.pk = false; });
286
317
  }
287
318
  render();
288
319
  return;
289
320
  }
290
- // Open type menu on type column
321
+
322
+ if (CYCLE_COLS[col]) {
323
+ rows[rowIdx][col] = cycleValue(col, rows[rowIdx][col]);
324
+ render();
325
+ return;
326
+ }
327
+
291
328
  if (col === 'type') {
292
329
  typeMenuOpen = true;
293
- typeMenuIdx = FIELD_TYPES.indexOf(rows[rowIdx].type || 'varchar');
294
- if (typeMenuIdx < 0) typeMenuIdx = 0;
330
+ typeMenuIdx = Math.max(0, FIELD_TYPES.indexOf(rows[rowIdx].type || 'varchar'));
295
331
  render();
296
332
  return;
297
333
  }
298
334
  }
299
335
 
300
- // Done
336
+ // ── Enter ────────────────────────────────────────────────────────────────
301
337
  if (key === KEYS.ENTER) {
302
338
  const col = cols[colIdx];
303
339
  if (col === 'type') { typeMenuOpen = true; typeMenuIdx = 0; render(); return; }
@@ -308,9 +344,9 @@ export async function fieldsTable(initialRows, cols, widths, title) {
308
344
  return;
309
345
  }
310
346
 
311
- // Text editing for current cell
347
+ // ── Text editing ─────────────────────────────────────────────────────────
312
348
  const col = cols[colIdx];
313
- if (col === 'pk' || col === 'fk') return;
349
+ if (BOOL_COLS.has(col) || CYCLE_COLS[col]) return; // not typed
314
350
 
315
351
  if (key === KEYS.BACKSPACE) {
316
352
  rows[rowIdx][col] = String(rows[rowIdx][col] || '').slice(0, -1);
@@ -318,7 +354,6 @@ export async function fieldsTable(initialRows, cols, widths, title) {
318
354
  return;
319
355
  }
320
356
 
321
- // Printable character
322
357
  if (key.length === 1 && key >= ' ') {
323
358
  rows[rowIdx][col] = (rows[rowIdx][col] || '') + key;
324
359
  render();