jsql-neo 5.4.2 → 5.5.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.
Files changed (72) hide show
  1. package/LICENSE +202 -202
  2. package/README.md +8097 -7971
  3. package/bin/jsql +320 -320
  4. package/bin/jsql-server +90 -90
  5. package/bin/jsql-server-control.js +177 -177
  6. package/index.d.ts +385 -357
  7. package/index.js +124 -118
  8. package/lib/ast.js +214 -0
  9. package/lib/btree.js +413 -413
  10. package/lib/cache.js +328 -328
  11. package/lib/client.js +125 -125
  12. package/lib/database.js +1744 -1744
  13. package/lib/date-types.js +182 -182
  14. package/lib/errors.js +89 -89
  15. package/lib/jsql_format.js +497 -497
  16. package/lib/migrate.js +254 -254
  17. package/lib/mod.js +292 -292
  18. package/lib/mongo_server.js +607 -607
  19. package/lib/multiserver.js +140 -140
  20. package/lib/mysql_compat.js +413 -413
  21. package/lib/mysql_server.js +1237 -1237
  22. package/lib/native_client.js +732 -732
  23. package/lib/nedb_compat.js +506 -506
  24. package/lib/pg_server.js +864 -864
  25. package/lib/plugin.js +34 -34
  26. package/lib/query.js +815 -815
  27. package/lib/redis_server.js +571 -571
  28. package/lib/sql.js +3460 -2925
  29. package/lib/sqlite_compat.js +323 -323
  30. package/lib/sqlite_worker.js +264 -264
  31. package/lib/table.js +1304 -1304
  32. package/lib/tui.js +502 -502
  33. package/lib/wasm.js +1 -1
  34. package/lib/wasm_client.js +718 -718
  35. package/lib/web_ui.js +257 -257
  36. package/native/jsql-neo-native.node +0 -0
  37. package/nativesrc/jsql-neo-core/Cargo.lock +455 -455
  38. package/nativesrc/jsql-neo-core/Cargo.toml +24 -24
  39. package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +455 -455
  40. package/nativesrc/jsql-neo-core/src/engine/memory.rs +168 -168
  41. package/nativesrc/jsql-neo-core/src/engine/mod.rs +62 -62
  42. package/nativesrc/jsql-neo-core/src/engine/table.rs +700 -700
  43. package/nativesrc/jsql-neo-core/src/lib.rs +2 -2
  44. package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -2
  45. package/nativesrc/jsql-neo-core/src/storage/persistent.rs +1 -1
  46. package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -85
  47. package/nativesrc/jsql-neo-core/src/types.rs +93 -93
  48. package/nativesrc/jsql-neo-native/Cargo.lock +606 -606
  49. package/nativesrc/jsql-neo-native/Cargo.toml +16 -16
  50. package/nativesrc/jsql-neo-native/build.rs +3 -3
  51. package/nativesrc/jsql-neo-native/package.json +7 -7
  52. package/nativesrc/jsql-neo-native/src/lib.rs +269 -269
  53. package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -465
  54. package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -15
  55. package/nativesrc/jsql-neo-wasm/src/lib.rs +447 -447
  56. package/package.json +87 -86
  57. package/postinstall.js +158 -158
  58. package/test/btree.test.js +128 -128
  59. package/test/coverage.js +187 -187
  60. package/test/join.test.js +110 -110
  61. package/test/native.test.js +158 -158
  62. package/test/regress-5.1.0.js +271 -271
  63. package/test/smoke.js +58 -58
  64. package/test/sql-parser.test.js +273 -0
  65. package/test/wasm.test.js +110 -110
  66. package/wasm/browser.d.ts +89 -89
  67. package/wasm/browser.mjs +569 -569
  68. package/wasm/browser_bg.mjs +597 -597
  69. package/wasm/jsql_neo_wasm.d.ts +38 -38
  70. package/wasm/jsql_neo_wasm.js +520 -520
  71. package/wasm/jsql_neo_wasm_bg.wasm.d.ts +26 -26
  72. package/wasm/package.json +11 -11
package/lib/tui.js CHANGED
@@ -1,503 +1,503 @@
1
- /*
2
- * jsql-neo TUI — zero-dependency interactive SQL terminal.
3
- *
4
- * Raw-mode keyboard handling, line editing with history & Tab completion,
5
- * statement continuation (quote/paren balance + trailing ";"), table
6
- * rendering with CJK-aware column widths, meta commands, and a status bar.
7
- *
8
- * When stdin is NOT a TTY it degrades to batch mode (execute each line).
9
- */
10
- const fs = require('fs');
11
- const path = require('path');
12
- const os = require('os');
13
- const Database = require('./database');
14
- const { executeSQL } = require('./sql');
15
-
16
- const HISTORY_FILE = path.join(os.homedir(), '.jsql-history');
17
- const MAX_HISTORY = 500;
18
-
19
- /* ---------- ANSI helpers ---------- */
20
-
21
- const RESET = '\x1b[0m';
22
- const BOLD = '\x1b[1m';
23
- const DIM = '\x1b[2m';
24
- const RED = '\x1b[31m';
25
- const GREEN = '\x1b[32m';
26
- const YELLOW = '\x1b[33m';
27
- const CYAN = '\x1b[36m';
28
- const MAGENTA = '\x1b[35m';
29
- const GRAY = '\x1b[90m';
30
- const CLEAR_LINE = '\x1b[2K';
31
- const CLEAR_SCREEN = '\x1b[2J';
32
- const HOME = '\x1b[H';
33
- const HIDE_CURSOR = '\x1b[?25l';
34
- const SHOW_CURSOR = '\x1b[?25h';
35
-
36
- /* ---------- display width (CJK-aware) ---------- */
37
-
38
- function wswidth(str) {
39
- let w = 0;
40
- for (const ch of String(str)) {
41
- const cp = ch.codePointAt(0);
42
- if (cp >= 0x1100 && (cp <= 0x115f || cp === 0x2329 || cp === 0x232a ||
43
- (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) ||
44
- (cp >= 0xac00 && cp <= 0xd7a3) || (cp >= 0xf900 && cp <= 0xfaff) ||
45
- (cp >= 0xfe30 && cp <= 0xfe4f) || (cp >= 0xff00 && cp <= 0xff60) ||
46
- (cp >= 0xffe0 && cp <= 0xffe6) || (cp >= 0x20000 && cp <= 0x2fffd))) {
47
- w += 2;
48
- } else w += 1;
49
- }
50
- return w;
51
- }
52
-
53
- function pad(str, width) {
54
- const s = String(str);
55
- return s + ' '.repeat(Math.max(0, width - wswidth(s)));
56
- }
57
-
58
- /* ---------- table rendering ---------- */
59
-
60
- function renderTable(headers, rows, maxWidth) {
61
- if (!headers || headers.length === 0) return '(no columns)';
62
- const widths = headers.map((h) => wswidth(h));
63
- const lines = [];
64
- for (const row of rows) {
65
- headers.forEach((_, i) => {
66
- const v = row[i] == null ? 'NULL' : String(row[i]);
67
- if (wswidth(v) > widths[i]) widths[i] = wswidth(v);
68
- });
69
- }
70
- const maxCol = maxWidth || 200;
71
- let total = 1;
72
- for (const w of widths) total += w + 3;
73
- if (total > maxCol && widths.length > 1) {
74
- let overshoot = total - maxCol;
75
- let i = widths.indexOf(Math.max(...widths));
76
- while (overshoot > 0) {
77
- if (i === -1 || widths[i] <= 3) break;
78
- const cut = Math.min(widths[i] - 3, overshoot);
79
- widths[i] -= cut;
80
- overshoot -= cut;
81
- }
82
- }
83
- const border = '+' + widths.map((w) => '-'.repeat(w + 2)).join('+') + '+';
84
- lines.push(border);
85
- lines.push('| ' + headers.map((h, i) => pad(h, widths[i])).join(' | ') + ' |');
86
- lines.push(border);
87
- for (const row of rows) {
88
- const cells = headers.map((_, i) => {
89
- const v = row[i] == null ? 'NULL' : String(row[i]);
90
- return v.length > widths[i] + 0 && widths[i] >= 3 && wswidth(v) > widths[i]
91
- ? v.slice(0, Math.max(1, widths[i] - 1)) + '…'
92
- : v;
93
- });
94
- lines.push('| ' + cells.map((c, i) => pad(c, widths[i])).join(' | ') + ' |');
95
- }
96
- lines.push(border);
97
- return lines.join('\n');
98
- }
99
-
100
- /* ---------- meta commands ---------- */
101
-
102
- const META_HELP = [
103
- ['\\q, \\quit, exit', '退出'],
104
- ['\\c', '清屏'],
105
- ['\\db', '显示当前数据库'],
106
- ['\\use <name>', '切换数据库'],
107
- ['\\tables', '列出所有表'],
108
- ['\\desc <table>', '查看表结构'],
109
- ['\\help', '显示帮助'],
110
- ['Ctrl+L 清屏 · Ctrl+C 取消当前行(再按退出) · Ctrl+D 退出', '快捷键'],
111
- ['Tab', '自动补全关键字'],
112
- ];
113
-
114
- /* ---------- TUI shell ---------- */
115
-
116
- class TUIShell {
117
- constructor(opts = {}) {
118
- this.dataDir = opts.dataDir || null;
119
- this.dbName = opts.db || 'default';
120
- this.engine = this._openEngine(this.dbName);
121
- this._engines = new Map();
122
- this._engines.set(this.dbName, this.engine);
123
- this.history = [];
124
- this._loadHistory();
125
- this.line = '';
126
- this.cursor = 0;
127
- this.histIdx = -1;
128
- this.pending = ''; // 续行 buffer
129
- this.dialect = opts.dialect || 'mysql';
130
- this.batch = !process.stdin.isTTY;
131
- this._exiting = false;
132
- }
133
-
134
- _openEngine(name) {
135
- if (this.dataDir && this.dataDir !== ':memory:') {
136
- return new Database(path.join(this.dataDir, name), { autoSave: true });
137
- }
138
- return new Database(':memory:', { autoSave: false });
139
- }
140
-
141
- _loadHistory() {
142
- try {
143
- const raw = fs.readFileSync(HISTORY_FILE, 'utf8').split('\n').filter(Boolean);
144
- this.history = raw.slice(-MAX_HISTORY);
145
- } catch (e) { this.history = []; }
146
- }
147
-
148
- _saveHistory() {
149
- try {
150
- const head = this.history.slice(-MAX_HISTORY);
151
- fs.writeFileSync(HISTORY_FILE, head.join('\n') + '\n');
152
- } catch (e) { /* ignore */ }
153
- }
154
-
155
- listen() { /* compat no-op (TUI owns the process) */ }
156
-
157
- async run() {
158
- if (this.batch) {
159
- return this._runBatch();
160
- }
161
- this._statusBar();
162
- console.log(`${CYAN}Welcome to jsql-neo ${BOLD}v${require('../package.json').version}${RESET} ${GRAY}(type \\help for commands)${RESET}\n`);
163
- process.stdin.setRawMode(true);
164
- process.stdin.resume();
165
- process.stdin.setEncoding('utf8');
166
- process.stdin.on('data', (chunk) => this._onKeys(chunk));
167
- process.stdout.write(HIDE_CURSOR);
168
- this._redrawPrompt();
169
- this._term = process.stdin;
170
- this._term.on('end', () => { this._saveHistory(); });
171
- return new Promise(() => {});
172
- }
173
-
174
- /* ---------- batch mode ---------- */
175
-
176
- async _runBatch() {
177
- let data = '';
178
- try { data = fs.readFileSync(0, 'utf8'); } catch (e) {}
179
- const lines = data.split('\n');
180
- let combined = '';
181
- let results = 0;
182
- for (const line of lines) {
183
- const trimmed = line.trim();
184
- if (combined === '' && (trimmed.startsWith('\\q') || trimmed.startsWith('exit') || trimmed.startsWith('quit'))) break;
185
- if (combined === '' && trimmed.startsWith('\\')) {
186
- const r = this._meta(trimmed, true);
187
- if (r === 'quit') break;
188
- continue;
189
- }
190
- if (trimmed === '') {
191
- if (combined !== '') { await this._exec(combined); results++; combined = ''; }
192
- continue;
193
- }
194
- combined += (combined ? '\n' : '') + line;
195
- if (this._statementComplete(combined)) { await this._exec(combined); results++; combined = ''; }
196
- }
197
- if (combined) { await this._exec(combined); results++; }
198
- if (results === 0) {
199
- this._err('(no SQL statements — use interactive mode for the TUI, e.g. `jsql tui`)');
200
- process.exitCode = 1;
201
- }
202
- }
203
-
204
- /* ---------- keyboard ---------- */
205
-
206
- _onKeys(chunk) {
207
- const seq = Buffer.from(chunk, 'utf8');
208
- let i = 0;
209
- while (i < seq.length) {
210
- const b = seq[i];
211
- if (b === 0x1b) {
212
- if (seq[i + 1] === 0x5b) {
213
- const c = seq[i + 2];
214
- if (c === 0x41) this._histPrev();
215
- else if (c === 0x42) this._histNext();
216
- else if (c === 0x43) this._moveRight();
217
- else if (c === 0x44) this._moveLeft();
218
- else if (c === 0x48 || c === 0x31) this.cursor = 0, this._redrawPrompt();
219
- else if (c === 0x46 || c === 0x34) this.cursor = this.line.length, this._redrawPrompt();
220
- else if (c === 0x33 && seq[i + 3] === 0x7e) { this._deleteAt(); i += 4; continue; }
221
- i += 3;
222
- continue;
223
- }
224
- if (seq[i + 1] === 0x4f) { // ESC O x
225
- const c = seq[i + 2];
226
- if (c === 0x48) this.cursor = 0;
227
- else if (c === 0x46) this.cursor = this.line.length;
228
- this._redrawPrompt();
229
- i += 3;
230
- continue;
231
- }
232
- i += 1; // lone ESC: ignore
233
- continue;
234
- }
235
- if (b === 0x0d || b === 0x0a) { this._enter(); i++; continue; }
236
- if (b === 0x7f || b === 0x08) { this._backspace(); i++; continue; }
237
- if (b === 0x01) { this.cursor = 0; this._redrawPrompt(); i++; continue; } // Ctrl+A
238
- if (b === 0x05) { this.cursor = this.line.length; this._redrawPrompt(); i++; continue; } // Ctrl+E
239
- if (b === 0x0c) { this._clearScreen(); i++; continue; } // Ctrl+L
240
- if (b === 0x03) { if (this.line || this.pending) this._cancelLine(); else this._quit(); i++; continue; } // Ctrl+C
241
- if (b === 0x04) { if (!this.line && !this.pending) this._quit(); i++; continue; } // Ctrl+D
242
- if (b === 0x09) { this._complete(); i++; continue; } // Tab
243
- if (b === 0x15) { this.line = ''; this.cursor = 0; this._redrawPrompt(); i++; continue; } // Ctrl+U
244
- if (b >= 0x20) {
245
- let ch, step;
246
- if (b < 0x80) { ch = String.fromCharCode(b); step = 1; }
247
- else {
248
- let n = 1;
249
- if (b >= 0xf0) n = 4;
250
- else if (b >= 0xe0) n = 3;
251
- else if (b >= 0xc0) n = 2;
252
- ch = seq.toString('utf8', i, i + n);
253
- step = ch.length || n;
254
- }
255
- this.line = this.line.slice(0, this.cursor) + ch + this.line.slice(this.cursor);
256
- this.cursor += ch.length;
257
- this._redrawPrompt();
258
- i += step;
259
- continue;
260
- }
261
- i++;
262
- }
263
- }
264
-
265
- _isMultibyte(seq, i) {
266
- const b = seq[i];
267
- let n = 1;
268
- if (b >= 0xf0) n = 4;
269
- else if (b >= 0xe0) n = 3;
270
- else if (b >= 0xc0) n = 2;
271
- return n > 1 && i + n <= seq.length;
272
- }
273
-
274
- _enter() {
275
- this._pushHistory(this.line);
276
- const text = this.line;
277
- this.line = '';
278
- this.cursor = 0;
279
- this.histIdx = -1;
280
- if (this.pending === '' && text.trim() === '') { this._redrawPrompt(); return; }
281
- this.pending += (this.pending ? '\n' : '') + text;
282
- if (this._statementComplete(this.pending)) {
283
- const stmt = this.pending;
284
- this.pending = '';
285
- this._exec(stmt);
286
- } else {
287
- this._redrawPrompt();
288
- }
289
- }
290
-
291
- _statementComplete(sql) {
292
- let inSQ = false, inDQ = false, parens = 0;
293
- for (let i = 0; i < sql.length; i++) {
294
- const c = sql[i];
295
- if (c === "'" && !inDQ) inSQ = !inSQ;
296
- else if (c === '"' && !inSQ) inDQ = !inDQ;
297
- else if (!inSQ && !inDQ) {
298
- if (c === '(') parens++;
299
- else if (c === ')') parens--;
300
- }
301
- }
302
- const trimmed = sql.trimEnd();
303
- return !inSQ && !inDQ && parens <= 0 && (trimmed.endsWith(';') || trimmed.endsWith('\\g'));
304
- }
305
-
306
- _backspace() {
307
- if (this.cursor <= 0) return;
308
- this.line = this.line.slice(0, this.cursor - 1) + this.line.slice(this.cursor);
309
- this.cursor--;
310
- this._redrawPrompt();
311
- }
312
-
313
- _deleteAt() {
314
- if (this.cursor >= this.line.length) return;
315
- this.line = this.line.slice(0, this.cursor) + this.line.slice(this.cursor + 1);
316
- this._redrawPrompt();
317
- }
318
-
319
- _moveLeft() { if (this.cursor > 0) { this.cursor--; this._redrawPrompt(); } }
320
- _moveRight() { if (this.cursor < this.line.length) { this.cursor++; this._redrawPrompt(); } }
321
-
322
- _histPrev() {
323
- if (this.history.length === 0) return;
324
- if (this.histIdx === -1) this.histIdx = this.history.length - 1;
325
- else if (this.histIdx > 0) this.histIdx--;
326
- this.line = this.history[this.histIdx];
327
- this.cursor = this.line.length;
328
- this._redrawPrompt();
329
- }
330
-
331
- _histNext() {
332
- if (this.histIdx === -1) return;
333
- this.histIdx++;
334
- if (this.histIdx >= this.history.length) { this.histIdx = -1; this.line = ''; }
335
- else this.line = this.history[this.histIdx];
336
- this.cursor = this.line.length;
337
- this._redrawPrompt();
338
- }
339
-
340
- _pushHistory(line) {
341
- const t = line.trim();
342
- if (!t || t.startsWith('\\')) return;
343
- if (this.history[this.history.length - 1] === t) return;
344
- this.history.push(t);
345
- if (this.history.length > MAX_HISTORY) this.history = this.history.slice(-MAX_HISTORY);
346
- this._saveHistory();
347
- }
348
-
349
- _cancelLine() {
350
- this.line = '';
351
- this.cursor = 0;
352
- if (this.pending) {
353
- this.pending = '';
354
- process.stdout.write('\r' + CLEAR_LINE + (this.pending ? '' : ''));
355
- }
356
- this._redrawPrompt();
357
- }
358
-
359
- _quit() {
360
- this._exiting = true;
361
- process.stdout.write('\r' + CLEAR_LINE + SHOW_CURSOR + '\n');
362
- this._saveHistory();
363
- try { process.stdin.setRawMode(false); } catch (e) {}
364
- process.stdin.pause();
365
- process.exit(0);
366
- }
367
-
368
- _redrawPrompt() {
369
- const prompt = this.pending ? `${GREEN} ->${RESET} ` : `${GREEN}jsql>${RESET} `;
370
- const display = this.line.slice(0, this.cursor) + this.line.slice(this.cursor);
371
- process.stdout.write('\r' + CLEAR_LINE + prompt + display);
372
- const left = wswidth(prompt + this.line.slice(0, this.cursor));
373
- if (left > 0) process.stdout.write(`\x1b[${left}D`);
374
- }
375
-
376
- _clearScreen() {
377
- process.stdout.write(CLEAR_SCREEN + HOME);
378
- this._statusBar();
379
- process.stdout.write('\n');
380
- this._redrawPrompt();
381
- }
382
-
383
- _statusBar() {
384
- const mode = this.dataDir ? `data: ${this.dataDir}` : 'in-memory';
385
- const bar = `${BOLD}${GREEN} jsql-neo ${RESET}${GRAY}v${require('../package.json').version}${RESET} | db: ${CYAN}${this.dbName}${RESET} | ${GRAY}${mode}${RESET} | dialect: ${this.dialect}`;
386
- process.stdout.write('\x1b[7m' + bar + RESET + '\n');
387
- }
388
-
389
- /* ---------- completion ---------- */
390
-
391
- _complete() {
392
- const keywords = ['SELECT', 'FROM', 'WHERE', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE',
393
- 'CREATE', 'TABLE', 'DROP', 'ALTER', 'ADD', 'COLUMN', 'PRIMARY', 'KEY', 'INDEX', 'AND', 'OR', 'NOT',
394
- 'NULL', 'LIKE', 'ILIKE', 'IN', 'BETWEEN', 'ORDER', 'BY', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET',
395
- 'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'ON', 'AS', 'DISTINCT', 'COUNT', 'SUM', 'AVG', 'MIN',
396
- 'MAX', 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'USE', 'EXPLAIN', 'UNION', 'ALL', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END'];
397
- const lastWord = this.line.slice(0, this.cursor).match(/[A-Za-z_][A-Za-z0-9_]*$/);
398
- if (!lastWord) { process.stdout.write('\a'); return; }
399
- const w = lastWord[0];
400
- const rest = this.line.slice(this.cursor);
401
- const match = keywords.find((k) => k.startsWith(w.toUpperCase()));
402
- if (!match) { process.stdout.write('\a'); return; }
403
- this.line = this.line.slice(0, this.cursor - w.length) + match + rest;
404
- this.cursor += match.length - w.length;
405
- this._redrawPrompt();
406
- }
407
-
408
- /* ---------- execution ---------- */
409
-
410
- async _exec(sql) {
411
- const start = Date.now();
412
- try {
413
- const result = await executeSQL(this.engine, sql, { dialect: this.dialect, safety: false });
414
- const elapsed = Date.now() - start;
415
- this._showResult(result, elapsed);
416
- } catch (e) {
417
- this._err(e.message || String(e));
418
- }
419
- }
420
-
421
- _showResult(result, elapsed) {
422
- if (result == null) { this._out(`(no result) ${GRAY}${elapsed} ms${RESET}`); return; }
423
- if (Array.isArray(result)) {
424
- result.forEach((r) => this._showResult(r, elapsed));
425
- this._redrawPrompt();
426
- return;
427
- }
428
- if (result.ok === true && result.columns && result.type === 'select') {
429
- const maxW = (process.stdout.columns || 120) - 4;
430
- this._out(renderTable(result.columns, result.rows, maxW));
431
- const n = result.rows.length;
432
- this._out(`${n} row${n === 1 ? '' : 's'} in set ${GRAY}(${elapsed} ms)${RESET}`);
433
- } else if (result.ok === true && (result.affectedRows !== undefined || result.rows !== undefined)) {
434
- const n = result.affectedRows !== undefined ? result.affectedRows : (Array.isArray(result.rows) ? result.rows.length : 0);
435
- this._out(`${GREEN}Query OK${RESET}, ${n} row${n === 1 ? '' : 's'} affected ${GRAY}(${elapsed} ms)${RESET}`);
436
- } else if (result.ok === true && result.message) {
437
- this._out(GREEN + result.message + RESET);
438
- } else if (result.ok === true && result.rows && Array.isArray(result.rows)) {
439
- this._out(renderTable(result.columns || Object.keys(result.rows[0] || {}), result.rows, (process.stdout.columns || 120) - 4));
440
- } else {
441
- this._out(JSON.stringify(result, null, 2));
442
- }
443
- }
444
-
445
- _out(text) {
446
- if (this.batch) { console.log(text.replace(/\x1b\[[0-9;]*m/g, '')); return; }
447
- process.stdout.write('\r' + CLEAR_LINE + text + '\n');
448
- this._redrawPrompt();
449
- }
450
-
451
- _err(msg) {
452
- if (this.batch) { console.error(msg.replace(/\x1b\[[0-9;]*m/g, '')); return; }
453
- process.stdout.write('\r' + CLEAR_LINE + `${RED}ERROR ${RESET}${msg}\n`);
454
- this._redrawPrompt();
455
- }
456
-
457
- /* ---------- meta ---------- */
458
-
459
- _meta(line, batch) {
460
- const out = (t) => (batch ? console.log(String(t).replace(/\x1b\[[0-9;]*m/g, '')) : this._out(t));
461
- const [head, ...rest] = line.trim().split(/\s+/);
462
- const arg = rest.join(' ');
463
- switch (head.toLowerCase()) {
464
- case '\\q': case '\\quit': case '\\exit': case 'exit': case 'quit':
465
- if (batch) return 'quit';
466
- this._quit(); break;
467
- case '\\c': case '\\clear':
468
- if (!batch) this._clearScreen(); break;
469
- case '\\db': return out(`current database: ${CYAN}${this.dbName}${RESET}`);
470
- case '\\use': {
471
- if (!arg) return out('usage: \\use <name>');
472
- if (!this._engines.has(arg)) this._engines.set(arg, this._openEngine(arg));
473
- this.engine = this._engines.get(arg);
474
- this.dbName = arg;
475
- return out(`switched to database: ${CYAN}${arg}${RESET}`);
476
- }
477
- case '\\tables': {
478
- const meta = this.engine._meta && this.engine._meta.tables ? Object.keys(this.engine._meta.tables) : [];
479
- const runtime = Object.keys(this.engine._tables || {});
480
- const tables = [...new Set([...meta, ...runtime])];
481
- if (tables.length === 0) return out('(no tables)');
482
- return out(renderTable(['Table'], tables.map((t) => [t]), (process.stdout.columns || 120) - 4));
483
- }
484
- case '\\desc': {
485
- if (!arg) return out('usage: \\desc <table>');
486
- const schema = this.engine.getTableSchema(arg);
487
- if (!schema) return out(`${RED}table not found:${RESET} ${arg}`);
488
- return out(renderTable(['Field', 'Type'], Object.entries(schema || {}).map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : String(v)]), (process.stdout.columns || 120) - 4));
489
- }
490
- case '\\help': case '\\?':
491
- return out(renderTable(['Command', 'Description'], META_HELP, (process.stdout.columns || 120) - 4));
492
- default:
493
- return out(`${RED}unknown meta command:${RESET} ${head} (try \\help)`);
494
- }
495
- return null;
496
- }
497
- }
498
-
499
- function createTUI(options) {
500
- return new TUIShell(options || {});
501
- }
502
-
1
+ /*
2
+ * jsql-neo TUI — zero-dependency interactive SQL terminal.
3
+ *
4
+ * Raw-mode keyboard handling, line editing with history & Tab completion,
5
+ * statement continuation (quote/paren balance + trailing ";"), table
6
+ * rendering with CJK-aware column widths, meta commands, and a status bar.
7
+ *
8
+ * When stdin is NOT a TTY it degrades to batch mode (execute each line).
9
+ */
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+ const Database = require('./database');
14
+ const { executeSQL } = require('./sql');
15
+
16
+ const HISTORY_FILE = path.join(os.homedir(), '.jsql-history');
17
+ const MAX_HISTORY = 500;
18
+
19
+ /* ---------- ANSI helpers ---------- */
20
+
21
+ const RESET = '\x1b[0m';
22
+ const BOLD = '\x1b[1m';
23
+ const DIM = '\x1b[2m';
24
+ const RED = '\x1b[31m';
25
+ const GREEN = '\x1b[32m';
26
+ const YELLOW = '\x1b[33m';
27
+ const CYAN = '\x1b[36m';
28
+ const MAGENTA = '\x1b[35m';
29
+ const GRAY = '\x1b[90m';
30
+ const CLEAR_LINE = '\x1b[2K';
31
+ const CLEAR_SCREEN = '\x1b[2J';
32
+ const HOME = '\x1b[H';
33
+ const HIDE_CURSOR = '\x1b[?25l';
34
+ const SHOW_CURSOR = '\x1b[?25h';
35
+
36
+ /* ---------- display width (CJK-aware) ---------- */
37
+
38
+ function wswidth(str) {
39
+ let w = 0;
40
+ for (const ch of String(str)) {
41
+ const cp = ch.codePointAt(0);
42
+ if (cp >= 0x1100 && (cp <= 0x115f || cp === 0x2329 || cp === 0x232a ||
43
+ (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) ||
44
+ (cp >= 0xac00 && cp <= 0xd7a3) || (cp >= 0xf900 && cp <= 0xfaff) ||
45
+ (cp >= 0xfe30 && cp <= 0xfe4f) || (cp >= 0xff00 && cp <= 0xff60) ||
46
+ (cp >= 0xffe0 && cp <= 0xffe6) || (cp >= 0x20000 && cp <= 0x2fffd))) {
47
+ w += 2;
48
+ } else w += 1;
49
+ }
50
+ return w;
51
+ }
52
+
53
+ function pad(str, width) {
54
+ const s = String(str);
55
+ return s + ' '.repeat(Math.max(0, width - wswidth(s)));
56
+ }
57
+
58
+ /* ---------- table rendering ---------- */
59
+
60
+ function renderTable(headers, rows, maxWidth) {
61
+ if (!headers || headers.length === 0) return '(no columns)';
62
+ const widths = headers.map((h) => wswidth(h));
63
+ const lines = [];
64
+ for (const row of rows) {
65
+ headers.forEach((_, i) => {
66
+ const v = row[i] == null ? 'NULL' : String(row[i]);
67
+ if (wswidth(v) > widths[i]) widths[i] = wswidth(v);
68
+ });
69
+ }
70
+ const maxCol = maxWidth || 200;
71
+ let total = 1;
72
+ for (const w of widths) total += w + 3;
73
+ if (total > maxCol && widths.length > 1) {
74
+ let overshoot = total - maxCol;
75
+ let i = widths.indexOf(Math.max(...widths));
76
+ while (overshoot > 0) {
77
+ if (i === -1 || widths[i] <= 3) break;
78
+ const cut = Math.min(widths[i] - 3, overshoot);
79
+ widths[i] -= cut;
80
+ overshoot -= cut;
81
+ }
82
+ }
83
+ const border = '+' + widths.map((w) => '-'.repeat(w + 2)).join('+') + '+';
84
+ lines.push(border);
85
+ lines.push('| ' + headers.map((h, i) => pad(h, widths[i])).join(' | ') + ' |');
86
+ lines.push(border);
87
+ for (const row of rows) {
88
+ const cells = headers.map((_, i) => {
89
+ const v = row[i] == null ? 'NULL' : String(row[i]);
90
+ return v.length > widths[i] + 0 && widths[i] >= 3 && wswidth(v) > widths[i]
91
+ ? v.slice(0, Math.max(1, widths[i] - 1)) + '…'
92
+ : v;
93
+ });
94
+ lines.push('| ' + cells.map((c, i) => pad(c, widths[i])).join(' | ') + ' |');
95
+ }
96
+ lines.push(border);
97
+ return lines.join('\n');
98
+ }
99
+
100
+ /* ---------- meta commands ---------- */
101
+
102
+ const META_HELP = [
103
+ ['\\q, \\quit, exit', '退出'],
104
+ ['\\c', '清屏'],
105
+ ['\\db', '显示当前数据库'],
106
+ ['\\use <name>', '切换数据库'],
107
+ ['\\tables', '列出所有表'],
108
+ ['\\desc <table>', '查看表结构'],
109
+ ['\\help', '显示帮助'],
110
+ ['Ctrl+L 清屏 · Ctrl+C 取消当前行(再按退出) · Ctrl+D 退出', '快捷键'],
111
+ ['Tab', '自动补全关键字'],
112
+ ];
113
+
114
+ /* ---------- TUI shell ---------- */
115
+
116
+ class TUIShell {
117
+ constructor(opts = {}) {
118
+ this.dataDir = opts.dataDir || null;
119
+ this.dbName = opts.db || 'default';
120
+ this.engine = this._openEngine(this.dbName);
121
+ this._engines = new Map();
122
+ this._engines.set(this.dbName, this.engine);
123
+ this.history = [];
124
+ this._loadHistory();
125
+ this.line = '';
126
+ this.cursor = 0;
127
+ this.histIdx = -1;
128
+ this.pending = ''; // 续行 buffer
129
+ this.dialect = opts.dialect || 'mysql';
130
+ this.batch = !process.stdin.isTTY;
131
+ this._exiting = false;
132
+ }
133
+
134
+ _openEngine(name) {
135
+ if (this.dataDir && this.dataDir !== ':memory:') {
136
+ return new Database(path.join(this.dataDir, name), { autoSave: true });
137
+ }
138
+ return new Database(':memory:', { autoSave: false });
139
+ }
140
+
141
+ _loadHistory() {
142
+ try {
143
+ const raw = fs.readFileSync(HISTORY_FILE, 'utf8').split('\n').filter(Boolean);
144
+ this.history = raw.slice(-MAX_HISTORY);
145
+ } catch (e) { this.history = []; }
146
+ }
147
+
148
+ _saveHistory() {
149
+ try {
150
+ const head = this.history.slice(-MAX_HISTORY);
151
+ fs.writeFileSync(HISTORY_FILE, head.join('\n') + '\n');
152
+ } catch (e) { /* ignore */ }
153
+ }
154
+
155
+ listen() { /* compat no-op (TUI owns the process) */ }
156
+
157
+ async run() {
158
+ if (this.batch) {
159
+ return this._runBatch();
160
+ }
161
+ this._statusBar();
162
+ console.log(`${CYAN}Welcome to jsql-neo ${BOLD}v${require('../package.json').version}${RESET} ${GRAY}(type \\help for commands)${RESET}\n`);
163
+ process.stdin.setRawMode(true);
164
+ process.stdin.resume();
165
+ process.stdin.setEncoding('utf8');
166
+ process.stdin.on('data', (chunk) => this._onKeys(chunk));
167
+ process.stdout.write(HIDE_CURSOR);
168
+ this._redrawPrompt();
169
+ this._term = process.stdin;
170
+ this._term.on('end', () => { this._saveHistory(); });
171
+ return new Promise(() => {});
172
+ }
173
+
174
+ /* ---------- batch mode ---------- */
175
+
176
+ async _runBatch() {
177
+ let data = '';
178
+ try { data = fs.readFileSync(0, 'utf8'); } catch (e) {}
179
+ const lines = data.split('\n');
180
+ let combined = '';
181
+ let results = 0;
182
+ for (const line of lines) {
183
+ const trimmed = line.trim();
184
+ if (combined === '' && (trimmed.startsWith('\\q') || trimmed.startsWith('exit') || trimmed.startsWith('quit'))) break;
185
+ if (combined === '' && trimmed.startsWith('\\')) {
186
+ const r = this._meta(trimmed, true);
187
+ if (r === 'quit') break;
188
+ continue;
189
+ }
190
+ if (trimmed === '') {
191
+ if (combined !== '') { await this._exec(combined); results++; combined = ''; }
192
+ continue;
193
+ }
194
+ combined += (combined ? '\n' : '') + line;
195
+ if (this._statementComplete(combined)) { await this._exec(combined); results++; combined = ''; }
196
+ }
197
+ if (combined) { await this._exec(combined); results++; }
198
+ if (results === 0) {
199
+ this._err('(no SQL statements — use interactive mode for the TUI, e.g. `jsql tui`)');
200
+ process.exitCode = 1;
201
+ }
202
+ }
203
+
204
+ /* ---------- keyboard ---------- */
205
+
206
+ _onKeys(chunk) {
207
+ const seq = Buffer.from(chunk, 'utf8');
208
+ let i = 0;
209
+ while (i < seq.length) {
210
+ const b = seq[i];
211
+ if (b === 0x1b) {
212
+ if (seq[i + 1] === 0x5b) {
213
+ const c = seq[i + 2];
214
+ if (c === 0x41) this._histPrev();
215
+ else if (c === 0x42) this._histNext();
216
+ else if (c === 0x43) this._moveRight();
217
+ else if (c === 0x44) this._moveLeft();
218
+ else if (c === 0x48 || c === 0x31) this.cursor = 0, this._redrawPrompt();
219
+ else if (c === 0x46 || c === 0x34) this.cursor = this.line.length, this._redrawPrompt();
220
+ else if (c === 0x33 && seq[i + 3] === 0x7e) { this._deleteAt(); i += 4; continue; }
221
+ i += 3;
222
+ continue;
223
+ }
224
+ if (seq[i + 1] === 0x4f) { // ESC O x
225
+ const c = seq[i + 2];
226
+ if (c === 0x48) this.cursor = 0;
227
+ else if (c === 0x46) this.cursor = this.line.length;
228
+ this._redrawPrompt();
229
+ i += 3;
230
+ continue;
231
+ }
232
+ i += 1; // lone ESC: ignore
233
+ continue;
234
+ }
235
+ if (b === 0x0d || b === 0x0a) { this._enter(); i++; continue; }
236
+ if (b === 0x7f || b === 0x08) { this._backspace(); i++; continue; }
237
+ if (b === 0x01) { this.cursor = 0; this._redrawPrompt(); i++; continue; } // Ctrl+A
238
+ if (b === 0x05) { this.cursor = this.line.length; this._redrawPrompt(); i++; continue; } // Ctrl+E
239
+ if (b === 0x0c) { this._clearScreen(); i++; continue; } // Ctrl+L
240
+ if (b === 0x03) { if (this.line || this.pending) this._cancelLine(); else this._quit(); i++; continue; } // Ctrl+C
241
+ if (b === 0x04) { if (!this.line && !this.pending) this._quit(); i++; continue; } // Ctrl+D
242
+ if (b === 0x09) { this._complete(); i++; continue; } // Tab
243
+ if (b === 0x15) { this.line = ''; this.cursor = 0; this._redrawPrompt(); i++; continue; } // Ctrl+U
244
+ if (b >= 0x20) {
245
+ let ch, step;
246
+ if (b < 0x80) { ch = String.fromCharCode(b); step = 1; }
247
+ else {
248
+ let n = 1;
249
+ if (b >= 0xf0) n = 4;
250
+ else if (b >= 0xe0) n = 3;
251
+ else if (b >= 0xc0) n = 2;
252
+ ch = seq.toString('utf8', i, i + n);
253
+ step = ch.length || n;
254
+ }
255
+ this.line = this.line.slice(0, this.cursor) + ch + this.line.slice(this.cursor);
256
+ this.cursor += ch.length;
257
+ this._redrawPrompt();
258
+ i += step;
259
+ continue;
260
+ }
261
+ i++;
262
+ }
263
+ }
264
+
265
+ _isMultibyte(seq, i) {
266
+ const b = seq[i];
267
+ let n = 1;
268
+ if (b >= 0xf0) n = 4;
269
+ else if (b >= 0xe0) n = 3;
270
+ else if (b >= 0xc0) n = 2;
271
+ return n > 1 && i + n <= seq.length;
272
+ }
273
+
274
+ _enter() {
275
+ this._pushHistory(this.line);
276
+ const text = this.line;
277
+ this.line = '';
278
+ this.cursor = 0;
279
+ this.histIdx = -1;
280
+ if (this.pending === '' && text.trim() === '') { this._redrawPrompt(); return; }
281
+ this.pending += (this.pending ? '\n' : '') + text;
282
+ if (this._statementComplete(this.pending)) {
283
+ const stmt = this.pending;
284
+ this.pending = '';
285
+ this._exec(stmt);
286
+ } else {
287
+ this._redrawPrompt();
288
+ }
289
+ }
290
+
291
+ _statementComplete(sql) {
292
+ let inSQ = false, inDQ = false, parens = 0;
293
+ for (let i = 0; i < sql.length; i++) {
294
+ const c = sql[i];
295
+ if (c === "'" && !inDQ) inSQ = !inSQ;
296
+ else if (c === '"' && !inSQ) inDQ = !inDQ;
297
+ else if (!inSQ && !inDQ) {
298
+ if (c === '(') parens++;
299
+ else if (c === ')') parens--;
300
+ }
301
+ }
302
+ const trimmed = sql.trimEnd();
303
+ return !inSQ && !inDQ && parens <= 0 && (trimmed.endsWith(';') || trimmed.endsWith('\\g'));
304
+ }
305
+
306
+ _backspace() {
307
+ if (this.cursor <= 0) return;
308
+ this.line = this.line.slice(0, this.cursor - 1) + this.line.slice(this.cursor);
309
+ this.cursor--;
310
+ this._redrawPrompt();
311
+ }
312
+
313
+ _deleteAt() {
314
+ if (this.cursor >= this.line.length) return;
315
+ this.line = this.line.slice(0, this.cursor) + this.line.slice(this.cursor + 1);
316
+ this._redrawPrompt();
317
+ }
318
+
319
+ _moveLeft() { if (this.cursor > 0) { this.cursor--; this._redrawPrompt(); } }
320
+ _moveRight() { if (this.cursor < this.line.length) { this.cursor++; this._redrawPrompt(); } }
321
+
322
+ _histPrev() {
323
+ if (this.history.length === 0) return;
324
+ if (this.histIdx === -1) this.histIdx = this.history.length - 1;
325
+ else if (this.histIdx > 0) this.histIdx--;
326
+ this.line = this.history[this.histIdx];
327
+ this.cursor = this.line.length;
328
+ this._redrawPrompt();
329
+ }
330
+
331
+ _histNext() {
332
+ if (this.histIdx === -1) return;
333
+ this.histIdx++;
334
+ if (this.histIdx >= this.history.length) { this.histIdx = -1; this.line = ''; }
335
+ else this.line = this.history[this.histIdx];
336
+ this.cursor = this.line.length;
337
+ this._redrawPrompt();
338
+ }
339
+
340
+ _pushHistory(line) {
341
+ const t = line.trim();
342
+ if (!t || t.startsWith('\\')) return;
343
+ if (this.history[this.history.length - 1] === t) return;
344
+ this.history.push(t);
345
+ if (this.history.length > MAX_HISTORY) this.history = this.history.slice(-MAX_HISTORY);
346
+ this._saveHistory();
347
+ }
348
+
349
+ _cancelLine() {
350
+ this.line = '';
351
+ this.cursor = 0;
352
+ if (this.pending) {
353
+ this.pending = '';
354
+ process.stdout.write('\r' + CLEAR_LINE + (this.pending ? '' : ''));
355
+ }
356
+ this._redrawPrompt();
357
+ }
358
+
359
+ _quit() {
360
+ this._exiting = true;
361
+ process.stdout.write('\r' + CLEAR_LINE + SHOW_CURSOR + '\n');
362
+ this._saveHistory();
363
+ try { process.stdin.setRawMode(false); } catch (e) {}
364
+ process.stdin.pause();
365
+ process.exit(0);
366
+ }
367
+
368
+ _redrawPrompt() {
369
+ const prompt = this.pending ? `${GREEN} ->${RESET} ` : `${GREEN}jsql>${RESET} `;
370
+ const display = this.line.slice(0, this.cursor) + this.line.slice(this.cursor);
371
+ process.stdout.write('\r' + CLEAR_LINE + prompt + display);
372
+ const left = wswidth(prompt + this.line.slice(0, this.cursor));
373
+ if (left > 0) process.stdout.write(`\x1b[${left}D`);
374
+ }
375
+
376
+ _clearScreen() {
377
+ process.stdout.write(CLEAR_SCREEN + HOME);
378
+ this._statusBar();
379
+ process.stdout.write('\n');
380
+ this._redrawPrompt();
381
+ }
382
+
383
+ _statusBar() {
384
+ const mode = this.dataDir ? `data: ${this.dataDir}` : 'in-memory';
385
+ const bar = `${BOLD}${GREEN} jsql-neo ${RESET}${GRAY}v${require('../package.json').version}${RESET} | db: ${CYAN}${this.dbName}${RESET} | ${GRAY}${mode}${RESET} | dialect: ${this.dialect}`;
386
+ process.stdout.write('\x1b[7m' + bar + RESET + '\n');
387
+ }
388
+
389
+ /* ---------- completion ---------- */
390
+
391
+ _complete() {
392
+ const keywords = ['SELECT', 'FROM', 'WHERE', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE',
393
+ 'CREATE', 'TABLE', 'DROP', 'ALTER', 'ADD', 'COLUMN', 'PRIMARY', 'KEY', 'INDEX', 'AND', 'OR', 'NOT',
394
+ 'NULL', 'LIKE', 'ILIKE', 'IN', 'BETWEEN', 'ORDER', 'BY', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET',
395
+ 'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'ON', 'AS', 'DISTINCT', 'COUNT', 'SUM', 'AVG', 'MIN',
396
+ 'MAX', 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'USE', 'EXPLAIN', 'UNION', 'ALL', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END'];
397
+ const lastWord = this.line.slice(0, this.cursor).match(/[A-Za-z_][A-Za-z0-9_]*$/);
398
+ if (!lastWord) { process.stdout.write('\a'); return; }
399
+ const w = lastWord[0];
400
+ const rest = this.line.slice(this.cursor);
401
+ const match = keywords.find((k) => k.startsWith(w.toUpperCase()));
402
+ if (!match) { process.stdout.write('\a'); return; }
403
+ this.line = this.line.slice(0, this.cursor - w.length) + match + rest;
404
+ this.cursor += match.length - w.length;
405
+ this._redrawPrompt();
406
+ }
407
+
408
+ /* ---------- execution ---------- */
409
+
410
+ async _exec(sql) {
411
+ const start = Date.now();
412
+ try {
413
+ const result = await executeSQL(this.engine, sql, { dialect: this.dialect, safety: false });
414
+ const elapsed = Date.now() - start;
415
+ this._showResult(result, elapsed);
416
+ } catch (e) {
417
+ this._err(e.message || String(e));
418
+ }
419
+ }
420
+
421
+ _showResult(result, elapsed) {
422
+ if (result == null) { this._out(`(no result) ${GRAY}${elapsed} ms${RESET}`); return; }
423
+ if (Array.isArray(result)) {
424
+ result.forEach((r) => this._showResult(r, elapsed));
425
+ this._redrawPrompt();
426
+ return;
427
+ }
428
+ if (result.ok === true && result.columns && result.type === 'select') {
429
+ const maxW = (process.stdout.columns || 120) - 4;
430
+ this._out(renderTable(result.columns, result.rows, maxW));
431
+ const n = result.rows.length;
432
+ this._out(`${n} row${n === 1 ? '' : 's'} in set ${GRAY}(${elapsed} ms)${RESET}`);
433
+ } else if (result.ok === true && (result.affectedRows !== undefined || result.rows !== undefined)) {
434
+ const n = result.affectedRows !== undefined ? result.affectedRows : (Array.isArray(result.rows) ? result.rows.length : 0);
435
+ this._out(`${GREEN}Query OK${RESET}, ${n} row${n === 1 ? '' : 's'} affected ${GRAY}(${elapsed} ms)${RESET}`);
436
+ } else if (result.ok === true && result.message) {
437
+ this._out(GREEN + result.message + RESET);
438
+ } else if (result.ok === true && result.rows && Array.isArray(result.rows)) {
439
+ this._out(renderTable(result.columns || Object.keys(result.rows[0] || {}), result.rows, (process.stdout.columns || 120) - 4));
440
+ } else {
441
+ this._out(JSON.stringify(result, null, 2));
442
+ }
443
+ }
444
+
445
+ _out(text) {
446
+ if (this.batch) { console.log(text.replace(/\x1b\[[0-9;]*m/g, '')); return; }
447
+ process.stdout.write('\r' + CLEAR_LINE + text + '\n');
448
+ this._redrawPrompt();
449
+ }
450
+
451
+ _err(msg) {
452
+ if (this.batch) { console.error(msg.replace(/\x1b\[[0-9;]*m/g, '')); return; }
453
+ process.stdout.write('\r' + CLEAR_LINE + `${RED}ERROR ${RESET}${msg}\n`);
454
+ this._redrawPrompt();
455
+ }
456
+
457
+ /* ---------- meta ---------- */
458
+
459
+ _meta(line, batch) {
460
+ const out = (t) => (batch ? console.log(String(t).replace(/\x1b\[[0-9;]*m/g, '')) : this._out(t));
461
+ const [head, ...rest] = line.trim().split(/\s+/);
462
+ const arg = rest.join(' ');
463
+ switch (head.toLowerCase()) {
464
+ case '\\q': case '\\quit': case '\\exit': case 'exit': case 'quit':
465
+ if (batch) return 'quit';
466
+ this._quit(); break;
467
+ case '\\c': case '\\clear':
468
+ if (!batch) this._clearScreen(); break;
469
+ case '\\db': return out(`current database: ${CYAN}${this.dbName}${RESET}`);
470
+ case '\\use': {
471
+ if (!arg) return out('usage: \\use <name>');
472
+ if (!this._engines.has(arg)) this._engines.set(arg, this._openEngine(arg));
473
+ this.engine = this._engines.get(arg);
474
+ this.dbName = arg;
475
+ return out(`switched to database: ${CYAN}${arg}${RESET}`);
476
+ }
477
+ case '\\tables': {
478
+ const meta = this.engine._meta && this.engine._meta.tables ? Object.keys(this.engine._meta.tables) : [];
479
+ const runtime = Object.keys(this.engine._tables || {});
480
+ const tables = [...new Set([...meta, ...runtime])];
481
+ if (tables.length === 0) return out('(no tables)');
482
+ return out(renderTable(['Table'], tables.map((t) => [t]), (process.stdout.columns || 120) - 4));
483
+ }
484
+ case '\\desc': {
485
+ if (!arg) return out('usage: \\desc <table>');
486
+ const schema = this.engine.getTableSchema(arg);
487
+ if (!schema) return out(`${RED}table not found:${RESET} ${arg}`);
488
+ return out(renderTable(['Field', 'Type'], Object.entries(schema || {}).map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : String(v)]), (process.stdout.columns || 120) - 4));
489
+ }
490
+ case '\\help': case '\\?':
491
+ return out(renderTable(['Command', 'Description'], META_HELP, (process.stdout.columns || 120) - 4));
492
+ default:
493
+ return out(`${RED}unknown meta command:${RESET} ${head} (try \\help)`);
494
+ }
495
+ return null;
496
+ }
497
+ }
498
+
499
+ function createTUI(options) {
500
+ return new TUIShell(options || {});
501
+ }
502
+
503
503
  module.exports = { TUIShell, createTUI, renderTable, wswidth, pad };