midql-cli 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -76,6 +76,7 @@ All values are sent as bound parameters — never interpolated into SQL — so i
76
76
 
77
77
  ## REPL reference
78
78
 
79
+ - Results adapt to your terminal: columns shrink to fit the window, and when a row is too wide to fit at all, MidQL switches to a vertical record view (one `column value` pair per line) automatically.
79
80
  - Input starting with a SQL keyword runs as raw SQL on a **pinned session connection**, so `BEGIN` / `COMMIT` / `ROLLBACK` work naturally; an open transaction shows a `[tx]` badge in the prompt.
80
81
  - **Tab** accepts the top completion (verbs → tables → columns → operators, context-aware in both NL and SQL modes). Columns reachable through a foreign key are annotated `(accounts, joined)`.
81
82
  - **↑/↓** history, **Ctrl+R** history search, **Ctrl+C** clear line (twice to exit), **Ctrl+A/E** home/end, **Esc** dismiss.
@@ -83,6 +84,9 @@ All values are sent as bound parameters — never interpolated into SQL — so i
83
84
  | Command | Description |
84
85
  |---|---|
85
86
  | `\c <profile\|url> [alias]` | connect (multiple connections at once) |
87
+ | `\profiles` | list saved profiles |
88
+ | `\profile add [name]` | create a profile with a guided step-by-step wizard (masked password input) |
89
+ | `\profile remove <name>` | delete a saved profile |
86
90
  | `\use <alias>` | switch the active connection |
87
91
  | `\connections` | list open connections |
88
92
  | `\disconnect <alias>` | close a connection |
@@ -12,10 +12,10 @@ import {
12
12
  startsWithSqlKeyword,
13
13
  tokenize,
14
14
  translate
15
- } from "./chunk-WN6T44OZ.js";
16
- import "./chunk-3CIK5I4M.js";
17
- import "./chunk-7WNCISNV.js";
18
- import "./chunk-3QAR6XBK.js";
15
+ } from "./chunk-XTMRD34N.js";
16
+ import "./chunk-4C62NPJ6.js";
17
+ import "./chunk-5GQDAM2V.js";
18
+ import "./chunk-5YB2C5KL.js";
19
19
  import "./chunk-PXDMSYWH.js";
20
20
  import {
21
21
  AmbiguityError,
@@ -36,6 +36,7 @@ function LineEditor({
36
36
  promptColor,
37
37
  disabled,
38
38
  history,
39
+ mask,
39
40
  onSubmit,
40
41
  onExitRequest,
41
42
  onChange,
@@ -153,9 +154,10 @@ function LineEditor({
153
154
  },
154
155
  { isActive: !disabled }
155
156
  );
156
- const before = buffer.slice(0, cursor);
157
- const at = buffer[cursor] ?? " ";
158
- const after = buffer.slice(cursor + 1);
157
+ const display = mask ? "\u2022".repeat(buffer.length) : buffer;
158
+ const before = display.slice(0, cursor);
159
+ const at = display[cursor] ?? " ";
160
+ const after = display.slice(cursor + 1);
159
161
  return /* @__PURE__ */ jsxs(Text, { children: [
160
162
  /* @__PURE__ */ jsx(Text, { color: promptColor, bold: true, children: promptText }),
161
163
  /* @__PURE__ */ jsx(Text, { children: before }),
@@ -408,8 +410,9 @@ function App({ controller, version, initialTarget }) {
408
410
  async (value, echoPrefix) => {
409
411
  setBuffer("");
410
412
  setSearchMode(false);
411
- if (value.trim().length > 0) {
412
- append([{ id: 0, kind: "echo", text: `${echoPrefix}${value}` }]);
413
+ const masked = controller.pendingConfirmation()?.mask === true;
414
+ if (value.trim().length > 0 || masked) {
415
+ append([{ id: 0, kind: "echo", text: `${echoPrefix}${masked ? "\u2022\u2022\u2022\u2022\u2022\u2022" : value}` }]);
413
416
  }
414
417
  setBusy(true);
415
418
  const result = await controller.handleInput(value);
@@ -423,17 +426,34 @@ function App({ controller, version, initialTarget }) {
423
426
  [append, controller, exit]
424
427
  );
425
428
  useEffect(() => {
426
- append([
429
+ const banner = [
427
430
  {
428
431
  id: 0,
429
432
  kind: "plain",
430
433
  text: `midql v${version} \u2014 plain English or raw SQL \xB7 \\help for commands \xB7 \\q to quit`
431
434
  }
432
- ]);
435
+ ];
436
+ if (!initialTarget) {
437
+ const profiles = controller.config.profiles;
438
+ if (profiles.length > 0) {
439
+ banner.push({
440
+ id: 0,
441
+ kind: "info",
442
+ text: `Saved profiles: ${profiles.map((profile) => profile.name).join(", ")} \u2014 connect with \\c <name>`
443
+ });
444
+ } else {
445
+ banner.push({
446
+ id: 0,
447
+ kind: "info",
448
+ text: "Get started: \\profile add (guided setup) or \\c postgres://user:pass@host:5432/db"
449
+ });
450
+ }
451
+ }
452
+ append(banner);
433
453
  if (initialTarget) {
434
454
  void runInput(`\\c ${initialTarget}`, "");
435
455
  }
436
- }, [append, initialTarget, runInput, version]);
456
+ }, [append, controller, initialTarget, runInput, version]);
437
457
  const info = controller.promptInfo();
438
458
  const pending = controller.pendingConfirmation();
439
459
  const connection = controller.manager.active();
@@ -492,7 +512,7 @@ function App({ controller, version, initialTarget }) {
492
512
  let promptColor;
493
513
  if (pending) {
494
514
  promptText = `${pending.prompt}> `;
495
- promptColor = "red";
515
+ promptColor = pending.prompt === "y/N" || pending.prompt.includes("confirm") ? "red" : "cyan";
496
516
  } else if (searchMode) {
497
517
  promptText = "(history search)> ";
498
518
  promptColor = "magenta";
@@ -520,6 +540,7 @@ function App({ controller, version, initialTarget }) {
520
540
  promptColor,
521
541
  disabled: busy,
522
542
  history: controller.inputHistory,
543
+ mask: pending?.mask === true,
523
544
  onSubmit: (value) => void runInput(value, promptText),
524
545
  onExitRequest: handleExitRequest,
525
546
  onChange: setBuffer,
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getProfile,
4
4
  resolvePassword
5
- } from "./chunk-3QAR6XBK.js";
5
+ } from "./chunk-5YB2C5KL.js";
6
6
  import {
7
7
  trackTransactionState
8
8
  } from "./chunk-PXDMSYWH.js";
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/repl/format.ts
4
+ import Table from "cli-table3";
5
+ import pc from "picocolors";
6
+ var MAX_ROWS_SHOWN = 50;
7
+ var ROW_LIMIT_BEFORE_TRUNCATION = 200;
8
+ var MAX_CELL_WIDTH = 40;
9
+ var MIN_CELL_WIDTH = 5;
10
+ var CHROME_PER_COLUMN = 3;
11
+ function cellText(value) {
12
+ if (value === null || value === void 0) {
13
+ return "\u2205";
14
+ }
15
+ if (value instanceof Date) {
16
+ return value.toISOString();
17
+ }
18
+ if (typeof value === "object") {
19
+ return JSON.stringify(value);
20
+ }
21
+ return String(value).replace(/\r?\n/g, " ");
22
+ }
23
+ function clip(text, width) {
24
+ if (text.length <= width) {
25
+ return text;
26
+ }
27
+ return `${text.slice(0, Math.max(1, width - 1))}\u2026`;
28
+ }
29
+ function terminalWidth() {
30
+ const columns = process.stdout.columns;
31
+ return typeof columns === "number" && columns > 20 ? columns : 100;
32
+ }
33
+ function fitColumnWidths(naturals, available) {
34
+ const widths = naturals.map((natural) => Math.min(natural, MAX_CELL_WIDTH));
35
+ const chrome = widths.length * CHROME_PER_COLUMN + 1;
36
+ let total = widths.reduce((sum, width) => sum + width, 0) + chrome;
37
+ if (total <= available) {
38
+ return widths;
39
+ }
40
+ while (total > available) {
41
+ let widestIndex = -1;
42
+ for (let index = 0; index < widths.length; index++) {
43
+ if (widths[index] > MIN_CELL_WIDTH && (widestIndex < 0 || widths[index] > widths[widestIndex])) {
44
+ widestIndex = index;
45
+ }
46
+ }
47
+ if (widestIndex < 0) {
48
+ return null;
49
+ }
50
+ widths[widestIndex] = widths[widestIndex] - 1;
51
+ total -= 1;
52
+ }
53
+ return widths;
54
+ }
55
+ function verticalRecords(result, shown, width) {
56
+ const labelWidth = Math.min(
57
+ Math.max(...result.columns.map((column) => column.length)),
58
+ Math.floor(width / 3)
59
+ );
60
+ const valueWidth = Math.max(10, width - labelWidth - 3);
61
+ const lines = [];
62
+ shown.forEach((row, index) => {
63
+ const header = `\u2500[ row ${index + 1} ]`;
64
+ lines.push(pc.cyan(header.padEnd(Math.min(width, labelWidth + valueWidth + 3), "\u2500")));
65
+ for (const column of result.columns) {
66
+ const label = clip(column, labelWidth).padEnd(labelWidth);
67
+ const raw = cellText(row[column]);
68
+ const value = raw === "\u2205" ? pc.dim("\u2205") : clip(raw, valueWidth);
69
+ lines.push(`${pc.cyan(label)} ${value}`);
70
+ }
71
+ });
72
+ return lines.join("\n");
73
+ }
74
+ function plainTable(head, rows) {
75
+ const table = new Table({
76
+ head: head.map((column) => pc.cyan(column)),
77
+ style: { head: [], border: [] }
78
+ });
79
+ for (const row of rows) {
80
+ table.push(row);
81
+ }
82
+ return table.toString();
83
+ }
84
+ function describeTable(table) {
85
+ const rows = table.columns.map((column) => {
86
+ const flags = [];
87
+ if (column.isPrimaryKey) {
88
+ flags.push("PK");
89
+ }
90
+ for (const fk of table.foreignKeys) {
91
+ const index = fk.columns.indexOf(column.name);
92
+ if (index >= 0) {
93
+ flags.push(`FK \u2192 ${fk.referencedTable}.${fk.referencedColumns[index]}`);
94
+ }
95
+ }
96
+ return [
97
+ column.name,
98
+ column.dataType,
99
+ column.nullable ? "yes" : "no",
100
+ column.hasDefault ? "yes" : "no",
101
+ flags.join(", ")
102
+ ];
103
+ });
104
+ return plainTable(["column", "type", "nullable", "default", "keys"], rows);
105
+ }
106
+ function formatTableList(schema) {
107
+ const rows = schema.tables.map((table) => [
108
+ table.name,
109
+ String(table.columns.length),
110
+ `~${table.approxRowCount}`
111
+ ]);
112
+ return plainTable(["table", "columns", "rows"], rows);
113
+ }
114
+ function formatResultSet(result, width = terminalWidth()) {
115
+ if (result.columns.length === 0) {
116
+ const verb = result.command || "OK";
117
+ return pc.green(
118
+ `${verb} \xB7 ${result.rowCount} row${result.rowCount === 1 ? "" : "s"} affected \xB7 ${result.durationMs}ms`
119
+ );
120
+ }
121
+ if (result.rows.length === 0) {
122
+ return pc.dim(`(no rows) \xB7 ${result.durationMs}ms`);
123
+ }
124
+ const truncate = result.rows.length > ROW_LIMIT_BEFORE_TRUNCATION;
125
+ const shown = truncate ? result.rows.slice(0, MAX_ROWS_SHOWN) : result.rows;
126
+ const naturals = result.columns.map((column) => {
127
+ let widest = column.length;
128
+ for (const row of shown) {
129
+ const length = cellText(row[column]).length;
130
+ if (length > widest) {
131
+ widest = length;
132
+ }
133
+ }
134
+ return Math.max(widest, MIN_CELL_WIDTH);
135
+ });
136
+ const widths = fitColumnWidths(naturals, width);
137
+ const lines = [];
138
+ if (widths === null) {
139
+ lines.push(verticalRecords(result, shown, width));
140
+ } else {
141
+ const table = new Table({
142
+ head: result.columns.map((column, index) => pc.cyan(clip(column, widths[index]))),
143
+ colWidths: widths.map((cellWidth) => cellWidth + 2),
144
+ style: { head: [], border: [] }
145
+ });
146
+ for (const row of shown) {
147
+ table.push(
148
+ result.columns.map((column, index) => {
149
+ const raw = cellText(row[column]);
150
+ return raw === "\u2205" ? pc.dim("\u2205") : clip(raw, widths[index]);
151
+ })
152
+ );
153
+ }
154
+ lines.push(table.toString());
155
+ }
156
+ if (truncate) {
157
+ lines.push(
158
+ pc.yellow(
159
+ `\u2026 showing first ${MAX_ROWS_SHOWN} of ${result.rows.length} rows (add 'first N' or use \\export)`
160
+ )
161
+ );
162
+ }
163
+ lines.push(
164
+ pc.dim(
165
+ `${result.rows.length} row${result.rows.length === 1 ? "" : "s"} \xB7 ${result.durationMs}ms`
166
+ )
167
+ );
168
+ return lines.join("\n");
169
+ }
170
+
171
+ export {
172
+ plainTable,
173
+ describeTable,
174
+ formatTableList,
175
+ formatResultSet
176
+ };
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ConnectionManager
4
- } from "./chunk-3CIK5I4M.js";
4
+ } from "./chunk-4C62NPJ6.js";
5
5
  import {
6
6
  formatResultSet
7
- } from "./chunk-7WNCISNV.js";
7
+ } from "./chunk-5GQDAM2V.js";
8
8
  import {
9
9
  historyFilePath,
10
10
  loadConfig
11
- } from "./chunk-3QAR6XBK.js";
11
+ } from "./chunk-5YB2C5KL.js";
12
12
  import {
13
13
  AmbiguityError,
14
14
  MidqlError,
@@ -1376,6 +1376,19 @@ function searchHistory(entries, term) {
1376
1376
  }
1377
1377
 
1378
1378
  // src/repl/controller.ts
1379
+ function confirmationPrompt(promptText, expectedText, execute) {
1380
+ return {
1381
+ prompt: promptText,
1382
+ async handle(input) {
1383
+ const normalized = input.trim();
1384
+ const confirmed = expectedText ? normalized === expectedText : /^y(es)?$/i.test(normalized);
1385
+ if (!confirmed) {
1386
+ return { blocks: [block("info", "Cancelled.")] };
1387
+ }
1388
+ return { blocks: await execute() };
1389
+ }
1390
+ };
1391
+ }
1379
1392
  function block(kind, text) {
1380
1393
  return { kind, text };
1381
1394
  }
@@ -1448,8 +1461,8 @@ var ReplController = class {
1448
1461
  pendingConfirmation() {
1449
1462
  return this.pending;
1450
1463
  }
1451
- setPending(confirmation) {
1452
- this.pending = confirmation;
1464
+ setPending(prompt) {
1465
+ this.pending = prompt;
1453
1466
  }
1454
1467
  async handleInput(rawInput) {
1455
1468
  const input = rawInput.trim();
@@ -1461,7 +1474,7 @@ var ReplController = class {
1461
1474
  }
1462
1475
  try {
1463
1476
  if (input.startsWith("\\")) {
1464
- const { dispatchMeta } = await import("./meta-NZ5Z6NYN.js");
1477
+ const { dispatchMeta } = await import("./meta-V6NMP2YL.js");
1465
1478
  return await dispatchMeta(this, input);
1466
1479
  }
1467
1480
  return await this.handleQuery(input);
@@ -1492,13 +1505,8 @@ var ReplController = class {
1492
1505
  async respondToPending(input) {
1493
1506
  const pending = this.pending;
1494
1507
  this.pending = null;
1495
- const normalized = input.trim();
1496
- const confirmed = pending.expectedText ? normalized === pending.expectedText : /^y(es)?$/i.test(normalized);
1497
- if (!confirmed) {
1498
- return { blocks: [block("info", "Cancelled.")] };
1499
- }
1500
1508
  try {
1501
- return { blocks: await pending.execute() };
1509
+ return await pending.handle(input);
1502
1510
  } catch (error) {
1503
1511
  return { blocks: [this.errorBlock(error)] };
1504
1512
  }
@@ -1535,7 +1543,7 @@ var ReplController = class {
1535
1543
  }
1536
1544
  }
1537
1545
  async runAst(connection, input, ast) {
1538
- const { executeAst } = await import("./execute-E45HSHNE.js");
1546
+ const { executeAst } = await import("./execute-FFUQSEFN.js");
1539
1547
  return executeAst(this, connection, input, ast);
1540
1548
  }
1541
1549
  async runRawSql(connection, sql) {
@@ -1567,19 +1575,15 @@ var ReplController = class {
1567
1575
  )
1568
1576
  );
1569
1577
  blocks.push(block("warn", `Type '${expected}' to confirm:`));
1570
- this.pending = {
1571
- prompt: `confirm ${expected}`,
1572
- expectedText: expected,
1573
- execute: () => this.executeRaw(connection, sql)
1574
- };
1578
+ this.pending = confirmationPrompt(
1579
+ `confirm ${expected}`,
1580
+ expected,
1581
+ () => this.executeRaw(connection, sql)
1582
+ );
1575
1583
  } else {
1576
1584
  blocks.push(block("warn", `\u26A0${prodBanner} ${verb} will modify ${table ?? "data"}.`));
1577
1585
  blocks.push(block("warn", "Proceed? [y/N]"));
1578
- this.pending = {
1579
- prompt: "y/N",
1580
- expectedText: null,
1581
- execute: () => this.executeRaw(connection, sql)
1582
- };
1586
+ this.pending = confirmationPrompt("y/N", null, () => this.executeRaw(connection, sql));
1583
1587
  }
1584
1588
  return { blocks };
1585
1589
  }
@@ -1636,6 +1640,7 @@ export {
1636
1640
  classifyRawSql,
1637
1641
  loadHistory,
1638
1642
  searchHistory,
1643
+ confirmationPrompt,
1639
1644
  block,
1640
1645
  startsWithSqlKeyword,
1641
1646
  hasSqlShape,
package/dist/cli.js CHANGED
@@ -9,8 +9,8 @@ async function startRepl(target) {
9
9
  const [{ render }, { createElement }, { App }, { ReplController }] = await Promise.all([
10
10
  import("ink"),
11
11
  import("react"),
12
- import("./App-W6AAY42M.js"),
13
- import("./controller-HDNDKU6K.js")
12
+ import("./App-WRKCMUDZ.js"),
13
+ import("./controller-WRZHP73H.js")
14
14
  ]);
15
15
  const controller = new ReplController();
16
16
  const instance = render(
@@ -37,7 +37,7 @@ program.argument("[connection]", "profile name or connection URL to connect on s
37
37
  });
38
38
  program.command("query <input>").description("run a single query (plain English or raw SQL) and exit").requiredOption("--db <target>", "profile name or connection URL").option("--yes", "allow destructive statements", false).option("--format <format>", "output format: table, json, csv", "table").action(async (input, options) => {
39
39
  try {
40
- const { runQueryCommand } = await import("./query-RCUXCOAF.js");
40
+ const { runQueryCommand } = await import("./query-4C3XZBC2.js");
41
41
  const format = ["table", "json", "csv"].includes(options.format) ? options.format : "table";
42
42
  process.exit(await runQueryCommand(input, { db: options.db, yes: options.yes, format }));
43
43
  } catch (error) {
@@ -46,7 +46,7 @@ program.command("query <input>").description("run a single query (plain English
46
46
  });
47
47
  program.command("backup [file]").description("back up a database via pg_dump / mysqldump").requiredOption("--db <target>", "profile name or connection URL").option("--sql", "write a plain SQL dump instead of the custom format", false).action(async (file, options) => {
48
48
  try {
49
- const { runBackupCommand } = await import("./maintenance-QHIUTVV3.js");
49
+ const { runBackupCommand } = await import("./maintenance-IAOCUGWQ.js");
50
50
  process.exit(await runBackupCommand(file, options));
51
51
  } catch (error) {
52
52
  fail(error);
@@ -54,7 +54,7 @@ program.command("backup [file]").description("back up a database via pg_dump / m
54
54
  });
55
55
  program.command("restore <file>").description("restore a database from a backup file").requiredOption("--db <target>", "profile name or connection URL").option("--yes", "confirm overwriting the target database", false).action(async (file, options) => {
56
56
  try {
57
- const { runRestoreCommand } = await import("./maintenance-QHIUTVV3.js");
57
+ const { runRestoreCommand } = await import("./maintenance-IAOCUGWQ.js");
58
58
  process.exit(await runRestoreCommand(file, options));
59
59
  } catch (error) {
60
60
  fail(error);
@@ -63,7 +63,7 @@ program.command("restore <file>").description("restore a database from a backup
63
63
  var profiles = program.command("profiles").description("manage saved connection profiles");
64
64
  profiles.command("list").description("list saved profiles").action(async () => {
65
65
  try {
66
- const { runProfilesList } = await import("./profiles-4QGMCJAO.js");
66
+ const { runProfilesList } = await import("./profiles-GGTSKNLB.js");
67
67
  runProfilesList();
68
68
  } catch (error) {
69
69
  fail(error);
@@ -71,7 +71,7 @@ profiles.command("list").description("list saved profiles").action(async () => {
71
71
  });
72
72
  profiles.command("add <name>").description("save a connection profile").requiredOption("--dialect <dialect>", "postgres or mysql").requiredOption("--host <host>", "database host").requiredOption("--database <database>", "database name").requiredOption("--user <user>", "database user").option("--port <port>", "database port").option("--password <password>", "password to store encrypted").option("--password-env <name>", "environment variable to read the password from").option("--env <env>", "environment tag: dev, staging, prod", "dev").option("--readonly", "open this connection read-only by default", false).option("--ssl", "connect over SSL", false).action(async (name, options) => {
73
73
  try {
74
- const { runProfilesAdd } = await import("./profiles-4QGMCJAO.js");
74
+ const { runProfilesAdd } = await import("./profiles-GGTSKNLB.js");
75
75
  process.exit(
76
76
  runProfilesAdd(name, {
77
77
  dialect: String(options.dialect),
@@ -92,7 +92,7 @@ profiles.command("add <name>").description("save a connection profile").required
92
92
  });
93
93
  profiles.command("remove <name>").description("delete a saved profile").action(async (name) => {
94
94
  try {
95
- const { runProfilesRemove } = await import("./profiles-4QGMCJAO.js");
95
+ const { runProfilesRemove } = await import("./profiles-GGTSKNLB.js");
96
96
  process.exit(runProfilesRemove(name));
97
97
  } catch (error) {
98
98
  fail(error);
@@ -2,17 +2,19 @@
2
2
  import {
3
3
  ReplController,
4
4
  block,
5
+ confirmationPrompt,
5
6
  hasSqlShape,
6
7
  startsWithSqlKeyword
7
- } from "./chunk-WN6T44OZ.js";
8
- import "./chunk-3CIK5I4M.js";
9
- import "./chunk-7WNCISNV.js";
10
- import "./chunk-3QAR6XBK.js";
8
+ } from "./chunk-XTMRD34N.js";
9
+ import "./chunk-4C62NPJ6.js";
10
+ import "./chunk-5GQDAM2V.js";
11
+ import "./chunk-5YB2C5KL.js";
11
12
  import "./chunk-PXDMSYWH.js";
12
13
  import "./chunk-VFC3HWTF.js";
13
14
  export {
14
15
  ReplController,
15
16
  block,
17
+ confirmationPrompt,
16
18
  hasSqlShape,
17
19
  startsWithSqlKeyword
18
20
  };
@@ -10,15 +10,16 @@ import {
10
10
  import {
11
11
  astIsWrite,
12
12
  block,
13
- classifyAst
14
- } from "./chunk-WN6T44OZ.js";
15
- import "./chunk-3CIK5I4M.js";
13
+ classifyAst,
14
+ confirmationPrompt
15
+ } from "./chunk-XTMRD34N.js";
16
+ import "./chunk-4C62NPJ6.js";
16
17
  import {
17
18
  describeTable,
18
19
  formatResultSet,
19
20
  formatTableList
20
- } from "./chunk-7WNCISNV.js";
21
- import "./chunk-3QAR6XBK.js";
21
+ } from "./chunk-5GQDAM2V.js";
22
+ import "./chunk-5YB2C5KL.js";
22
23
  import "./chunk-PXDMSYWH.js";
23
24
  import {
24
25
  ParseError
@@ -87,11 +88,13 @@ function confirmDropTable(controller, connection, input, table, built) {
87
88
  const meta = connection.schema.tables.find((entry) => entry.name === table);
88
89
  const rowNote = meta ? ` (~${meta.approxRowCount} rows)` : "";
89
90
  const prodBanner = connection.env === "prod" ? " (PRODUCTION)" : "";
90
- controller.setPending({
91
- prompt: `type '${table}' to confirm`,
92
- expectedText: table,
93
- execute: () => runBuilt(controller, connection, input, built)
94
- });
91
+ controller.setPending(
92
+ confirmationPrompt(
93
+ `type '${table}' to confirm`,
94
+ table,
95
+ () => runBuilt(controller, connection, input, built)
96
+ )
97
+ );
95
98
  return {
96
99
  blocks: [
97
100
  block("sql", `\u2192 ${built.sql}`),
@@ -135,18 +138,18 @@ async function confirmDestructive(controller, connection, input, ast, built, dan
135
138
  }
136
139
  if (danger === "catastrophic") {
137
140
  blocks.push(block("warn", `Type '${ast.table}' to confirm:`));
138
- controller.setPending({
139
- prompt: `type '${ast.table}' to confirm`,
140
- expectedText: ast.table,
141
- execute: () => runBuilt(controller, connection, input, built)
142
- });
141
+ controller.setPending(
142
+ confirmationPrompt(
143
+ `type '${ast.table}' to confirm`,
144
+ ast.table,
145
+ () => runBuilt(controller, connection, input, built)
146
+ )
147
+ );
143
148
  } else {
144
149
  blocks.push(block("warn", "Proceed? [y/N]"));
145
- controller.setPending({
146
- prompt: "y/N",
147
- expectedText: null,
148
- execute: () => runBuilt(controller, connection, input, built)
149
- });
150
+ controller.setPending(
151
+ confirmationPrompt("y/N", null, () => runBuilt(controller, connection, input, built))
152
+ );
150
153
  }
151
154
  return { blocks };
152
155
  }
@@ -1,4 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ resolveTarget
4
+ } from "./chunk-4C62NPJ6.js";
5
+ import {
6
+ loadConfig
7
+ } from "./chunk-5YB2C5KL.js";
8
+ import "./chunk-PXDMSYWH.js";
2
9
  import {
3
10
  backupDatabase,
4
11
  defaultBackupFile
@@ -7,13 +14,6 @@ import {
7
14
  restoreDatabase
8
15
  } from "./chunk-3FPZQHHV.js";
9
16
  import "./chunk-NCN3ZBOJ.js";
10
- import {
11
- resolveTarget
12
- } from "./chunk-3CIK5I4M.js";
13
- import {
14
- loadConfig
15
- } from "./chunk-3QAR6XBK.js";
16
- import "./chunk-PXDMSYWH.js";
17
17
  import "./chunk-VFC3HWTF.js";
18
18
 
19
19
  // src/commands/maintenance.ts
@@ -1,16 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  block,
4
+ confirmationPrompt,
4
5
  loadHistory,
5
6
  searchHistory
6
- } from "./chunk-WN6T44OZ.js";
7
- import "./chunk-3CIK5I4M.js";
7
+ } from "./chunk-XTMRD34N.js";
8
+ import "./chunk-4C62NPJ6.js";
8
9
  import {
9
10
  describeTable,
10
11
  formatTableList,
11
12
  plainTable
12
- } from "./chunk-7WNCISNV.js";
13
- import "./chunk-3QAR6XBK.js";
13
+ } from "./chunk-5GQDAM2V.js";
14
+ import "./chunk-5YB2C5KL.js";
14
15
  import "./chunk-PXDMSYWH.js";
15
16
  import "./chunk-VFC3HWTF.js";
16
17
 
@@ -213,6 +214,72 @@ var commands = {
213
214
  };
214
215
  }
215
216
  },
217
+ profiles: {
218
+ usage: "\\profiles",
219
+ description: "List saved connection profiles",
220
+ async run() {
221
+ const { listProfiles } = await import("./profiles-SW242OOI.js");
222
+ const profiles = listProfiles();
223
+ if (profiles.length === 0) {
224
+ return {
225
+ blocks: [
226
+ block("info", "No saved profiles yet."),
227
+ block("plain", "Create one with \\profile add \u2014 it walks you through every field.")
228
+ ]
229
+ };
230
+ }
231
+ const rows = profiles.map((profile) => [
232
+ profile.name,
233
+ profile.dialect,
234
+ `${profile.user}@${profile.host}:${profile.port}/${profile.database}`,
235
+ profile.env,
236
+ profile.readonly ? "read-only" : "read-write",
237
+ profile.encryptedPassword ? "stored" : profile.passwordEnv ? `env:${profile.passwordEnv}` : "none"
238
+ ]);
239
+ return {
240
+ blocks: [
241
+ block(
242
+ "table",
243
+ plainTable(["name", "dialect", "target", "env", "mode", "password"], rows)
244
+ ),
245
+ block(
246
+ "plain",
247
+ "Connect: \\c <name> \xB7 Add: \\profile add \xB7 Remove: \\profile remove <name>"
248
+ )
249
+ ]
250
+ };
251
+ }
252
+ },
253
+ profile: {
254
+ usage: "\\profile add [name] | remove <name>",
255
+ description: "Create a profile step by step, or delete one",
256
+ async run(controller, args) {
257
+ const action = args[0]?.toLowerCase();
258
+ if (action === "add" || action === "new" || action === "create") {
259
+ const { startProfileWizard } = await import("./profileWizard-7GJFTDLY.js");
260
+ return startProfileWizard(controller, args[1]);
261
+ }
262
+ if (action === "remove" || action === "delete" || action === "rm") {
263
+ const name = args[1];
264
+ if (!name) {
265
+ return { blocks: [block("error", "Usage: \\profile remove <name>")] };
266
+ }
267
+ const { getProfile } = await import("./profiles-SW242OOI.js");
268
+ if (!getProfile(name)) {
269
+ return { blocks: [block("error", `Profile "${name}" not found. See \\profiles.`)] };
270
+ }
271
+ controller.setPending(
272
+ confirmationPrompt("y/N", null, async () => {
273
+ const { deleteProfile } = await import("./profiles-SW242OOI.js");
274
+ deleteProfile(name);
275
+ return [block("success", `Profile "${name}" removed.`)];
276
+ })
277
+ );
278
+ return { blocks: [block("warn", `Remove profile "${name}"? [y/N]`)] };
279
+ }
280
+ return { blocks: [block("error", "Usage: \\profile add [name] | remove <name>")] };
281
+ }
282
+ },
216
283
  export: {
217
284
  usage: "\\export csv|json <file>",
218
285
  description: "Export the last result set to a file",
@@ -277,21 +344,23 @@ var commands = {
277
344
  return { blocks: [block("error", "Usage: \\restore <file>")] };
278
345
  }
279
346
  const prodBanner = connection.env === "prod" ? " (PRODUCTION)" : "";
280
- controller.setPending({
281
- prompt: `type '${connection.database}' to confirm`,
282
- expectedText: connection.database,
283
- async execute() {
284
- const { restoreDatabase } = await import("./restore-4QXG2WRR.js");
285
- await restoreDatabase(
286
- connection.dialect,
287
- connection.options,
288
- file,
289
- controller.config.tools
290
- );
291
- await controller.manager.refreshSchema(connection.alias).catch(() => connection);
292
- return [block("success", `Restored ${connection.database} from ${file}.`)];
293
- }
294
- });
347
+ controller.setPending(
348
+ confirmationPrompt(
349
+ `type '${connection.database}' to confirm`,
350
+ connection.database,
351
+ async () => {
352
+ const { restoreDatabase } = await import("./restore-4QXG2WRR.js");
353
+ await restoreDatabase(
354
+ connection.dialect,
355
+ connection.options,
356
+ file,
357
+ controller.config.tools
358
+ );
359
+ await controller.manager.refreshSchema(connection.alias).catch(() => connection);
360
+ return [block("success", `Restored ${connection.database} from ${file}.`)];
361
+ }
362
+ )
363
+ );
295
364
  return {
296
365
  blocks: [
297
366
  block(
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ block
4
+ } from "./chunk-XTMRD34N.js";
5
+ import "./chunk-4C62NPJ6.js";
6
+ import "./chunk-5GQDAM2V.js";
7
+ import {
8
+ getProfile,
9
+ saveProfile
10
+ } from "./chunk-5YB2C5KL.js";
11
+ import "./chunk-PXDMSYWH.js";
12
+ import "./chunk-VFC3HWTF.js";
13
+
14
+ // src/repl/profileWizard.ts
15
+ var steps = [
16
+ {
17
+ key: "name",
18
+ prompt: "profile name",
19
+ question: () => "Profile name (e.g. main, staging-db):",
20
+ apply(value, state) {
21
+ const name = value.trim();
22
+ if (!/^[\w-]+$/.test(name)) {
23
+ return "Use letters, numbers, - or _ only.";
24
+ }
25
+ if (getProfile(name)) {
26
+ return `Profile "${name}" already exists \u2014 pick another name, or remove it first with \\profile remove ${name}.`;
27
+ }
28
+ state.name = name;
29
+ return null;
30
+ }
31
+ },
32
+ {
33
+ key: "dialect",
34
+ prompt: "dialect",
35
+ question: () => "Database type \u2014 postgres or mysql [postgres]:",
36
+ apply(value, state) {
37
+ const dialect = value.trim().toLowerCase() || "postgres";
38
+ if (dialect !== "postgres" && dialect !== "mysql") {
39
+ return `"${value.trim()}" is not supported \u2014 type postgres or mysql.`;
40
+ }
41
+ state.dialect = dialect;
42
+ return null;
43
+ }
44
+ },
45
+ {
46
+ key: "host",
47
+ prompt: "host",
48
+ question: () => "Host [localhost]:",
49
+ apply(value, state) {
50
+ state.host = value.trim() || "localhost";
51
+ return null;
52
+ }
53
+ },
54
+ {
55
+ key: "port",
56
+ prompt: "port",
57
+ question: (state) => `Port [${state.dialect === "mysql" ? 3306 : 5432}]:`,
58
+ apply(value, state) {
59
+ const fallback = state.dialect === "mysql" ? 3306 : 5432;
60
+ const trimmed = value.trim();
61
+ if (!trimmed) {
62
+ state.port = fallback;
63
+ return null;
64
+ }
65
+ const port = Number(trimmed);
66
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
67
+ return `"${trimmed}" is not a valid port.`;
68
+ }
69
+ state.port = port;
70
+ return null;
71
+ }
72
+ },
73
+ {
74
+ key: "database",
75
+ prompt: "database",
76
+ question: () => "Database name:",
77
+ apply(value, state) {
78
+ const database = value.trim();
79
+ if (!database) {
80
+ return "Database name is required.";
81
+ }
82
+ state.database = database;
83
+ return null;
84
+ }
85
+ },
86
+ {
87
+ key: "user",
88
+ prompt: "user",
89
+ question: () => "User:",
90
+ apply(value, state) {
91
+ const user = value.trim();
92
+ if (!user) {
93
+ return "User is required.";
94
+ }
95
+ state.user = user;
96
+ return null;
97
+ }
98
+ },
99
+ {
100
+ key: "password",
101
+ prompt: "password",
102
+ question: () => "Password \u2014 stored encrypted. Empty = none, or env:VAR_NAME to read from an environment variable:",
103
+ mask: true,
104
+ apply(value, state) {
105
+ const trimmed = value.trim();
106
+ if (!trimmed) {
107
+ state.password = null;
108
+ state.passwordEnv = null;
109
+ } else if (trimmed.toLowerCase().startsWith("env:")) {
110
+ state.password = null;
111
+ state.passwordEnv = trimmed.slice(4);
112
+ } else {
113
+ state.password = value;
114
+ state.passwordEnv = null;
115
+ }
116
+ return null;
117
+ }
118
+ },
119
+ {
120
+ key: "env",
121
+ prompt: "environment",
122
+ question: () => "Environment \u2014 dev, staging or prod [dev]:",
123
+ apply(value, state) {
124
+ const env = value.trim().toLowerCase() || "dev";
125
+ if (env !== "dev" && env !== "staging" && env !== "prod") {
126
+ return `"${value.trim()}" is not valid \u2014 type dev, staging or prod.`;
127
+ }
128
+ state.env = env;
129
+ return null;
130
+ }
131
+ },
132
+ {
133
+ key: "readonly",
134
+ prompt: "read-only",
135
+ question: () => "Open read-only by default? y/N:",
136
+ apply(value, state) {
137
+ state.readonly = /^y(es)?$/i.test(value.trim());
138
+ return null;
139
+ }
140
+ }
141
+ ];
142
+ function finishWizard(state) {
143
+ const profile = {
144
+ name: state.name,
145
+ dialect: state.dialect,
146
+ host: state.host,
147
+ port: state.port,
148
+ database: state.database,
149
+ user: state.user,
150
+ encryptedPassword: null,
151
+ passwordEnv: state.passwordEnv ?? null,
152
+ env: state.env,
153
+ readonly: state.readonly ?? false,
154
+ ssl: false
155
+ };
156
+ saveProfile(profile, state.password ?? void 0);
157
+ const blocks = [
158
+ block(
159
+ "success",
160
+ `Profile "${profile.name}" saved \u2014 ${profile.dialect}://${profile.user}@${profile.host}:${profile.port}/${profile.database} (${profile.env}${profile.readonly ? ", read-only" : ""})`
161
+ ),
162
+ block("info", `Connect with: \\c ${profile.name}`)
163
+ ];
164
+ if (state.password) {
165
+ blocks.push(block("plain", "Password stored encrypted in ~/.midql (key file: ~/.midql/.key)."));
166
+ }
167
+ return blocks;
168
+ }
169
+ function askStep(controller, index, state, prefix) {
170
+ const step = steps[index];
171
+ if (!step) {
172
+ return { blocks: [...prefix, ...finishWizard(state)] };
173
+ }
174
+ controller.setPending({
175
+ prompt: step.prompt,
176
+ mask: step.mask,
177
+ async handle(input) {
178
+ if (input.trim().toLowerCase() === "cancel") {
179
+ return { blocks: [block("info", "Profile setup cancelled.")] };
180
+ }
181
+ const error = step.apply(input, state);
182
+ if (error) {
183
+ return askStep(controller, index, state, [block("warn", error)]);
184
+ }
185
+ return askStep(controller, index + 1, state, []);
186
+ }
187
+ });
188
+ return { blocks: [...prefix, block("info", step.question(state))] };
189
+ }
190
+ function startProfileWizard(controller, name) {
191
+ const state = {};
192
+ const intro = [
193
+ block("plain", "New connection profile \u2014 answer each question, type 'cancel' to abort.")
194
+ ];
195
+ if (name) {
196
+ const error = steps[0].apply(name, state);
197
+ if (error) {
198
+ return askStep(controller, 0, state, [...intro, block("warn", error)]);
199
+ }
200
+ return askStep(controller, 1, state, intro);
201
+ }
202
+ return askStep(controller, 0, state, intro);
203
+ }
204
+ export {
205
+ startProfileWizard
206
+ };
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  plainTable
4
- } from "./chunk-7WNCISNV.js";
4
+ } from "./chunk-5GQDAM2V.js";
5
5
  import {
6
6
  deleteProfile,
7
7
  listProfiles,
8
8
  saveProfile
9
- } from "./chunk-3QAR6XBK.js";
9
+ } from "./chunk-5YB2C5KL.js";
10
10
  import "./chunk-VFC3HWTF.js";
11
11
 
12
12
  // src/commands/profiles.ts
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ deleteProfile,
4
+ getProfile,
5
+ listProfiles,
6
+ resolvePassword,
7
+ saveProfile
8
+ } from "./chunk-5YB2C5KL.js";
9
+ import "./chunk-VFC3HWTF.js";
10
+ export {
11
+ deleteProfile,
12
+ getProfile,
13
+ listProfiles,
14
+ resolvePassword,
15
+ saveProfile
16
+ };
@@ -6,12 +6,6 @@ import {
6
6
  buildQuery,
7
7
  dialectFor
8
8
  } from "./chunk-TLTYYFT5.js";
9
- import {
10
- toCsv
11
- } from "./chunk-XJAN6OQ7.js";
12
- import {
13
- toJson
14
- } from "./chunk-GWY47EDH.js";
15
9
  import {
16
10
  astIsWrite,
17
11
  classifyAst,
@@ -20,20 +14,26 @@ import {
20
14
  rawSqlIsWrite,
21
15
  startsWithSqlKeyword,
22
16
  translate
23
- } from "./chunk-WN6T44OZ.js";
17
+ } from "./chunk-XTMRD34N.js";
24
18
  import {
25
19
  PostgresAdapter,
26
20
  resolveTarget
27
- } from "./chunk-3CIK5I4M.js";
21
+ } from "./chunk-4C62NPJ6.js";
28
22
  import {
29
23
  describeTable,
30
24
  formatResultSet,
31
25
  formatTableList
32
- } from "./chunk-7WNCISNV.js";
26
+ } from "./chunk-5GQDAM2V.js";
33
27
  import {
34
28
  loadConfig
35
- } from "./chunk-3QAR6XBK.js";
29
+ } from "./chunk-5YB2C5KL.js";
36
30
  import "./chunk-PXDMSYWH.js";
31
+ import {
32
+ toCsv
33
+ } from "./chunk-XJAN6OQ7.js";
34
+ import {
35
+ toJson
36
+ } from "./chunk-GWY47EDH.js";
37
37
  import "./chunk-VFC3HWTF.js";
38
38
 
39
39
  // src/commands/query.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "midql-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Fast, safety-first PostgreSQL/MySQL CLI for developers — schema-aware autocomplete, natural-language shortcuts, destructive-op previews",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,106 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/repl/format.ts
4
- import Table from "cli-table3";
5
- import pc from "picocolors";
6
- var MAX_ROWS_SHOWN = 50;
7
- var ROW_LIMIT_BEFORE_TRUNCATION = 200;
8
- var MAX_CELL_WIDTH = 40;
9
- function formatCell(value) {
10
- if (value === null || value === void 0) {
11
- return pc.dim("\u2205");
12
- }
13
- if (value instanceof Date) {
14
- return value.toISOString();
15
- }
16
- if (typeof value === "object") {
17
- return JSON.stringify(value);
18
- }
19
- const text = String(value);
20
- if (text.length > MAX_CELL_WIDTH) {
21
- return `${text.slice(0, MAX_CELL_WIDTH - 1)}\u2026`;
22
- }
23
- return text;
24
- }
25
- function plainTable(head, rows) {
26
- const table = new Table({
27
- head: head.map((column) => pc.cyan(column)),
28
- style: { head: [], border: [] }
29
- });
30
- for (const row of rows) {
31
- table.push(row);
32
- }
33
- return table.toString();
34
- }
35
- function describeTable(table) {
36
- const rows = table.columns.map((column) => {
37
- const flags = [];
38
- if (column.isPrimaryKey) {
39
- flags.push("PK");
40
- }
41
- for (const fk of table.foreignKeys) {
42
- const index = fk.columns.indexOf(column.name);
43
- if (index >= 0) {
44
- flags.push(`FK \u2192 ${fk.referencedTable}.${fk.referencedColumns[index]}`);
45
- }
46
- }
47
- return [
48
- column.name,
49
- column.dataType,
50
- column.nullable ? "yes" : "no",
51
- column.hasDefault ? "yes" : "no",
52
- flags.join(", ")
53
- ];
54
- });
55
- return plainTable(["column", "type", "nullable", "default", "keys"], rows);
56
- }
57
- function formatTableList(schema) {
58
- const rows = schema.tables.map((table) => [
59
- table.name,
60
- String(table.columns.length),
61
- `~${table.approxRowCount}`
62
- ]);
63
- return plainTable(["table", "columns", "rows"], rows);
64
- }
65
- function formatResultSet(result) {
66
- if (result.columns.length === 0) {
67
- const verb = result.command || "OK";
68
- return pc.green(
69
- `${verb} \xB7 ${result.rowCount} row${result.rowCount === 1 ? "" : "s"} affected \xB7 ${result.durationMs}ms`
70
- );
71
- }
72
- if (result.rows.length === 0) {
73
- return pc.dim(`(no rows) \xB7 ${result.durationMs}ms`);
74
- }
75
- const truncate = result.rows.length > ROW_LIMIT_BEFORE_TRUNCATION;
76
- const shown = truncate ? result.rows.slice(0, MAX_ROWS_SHOWN) : result.rows;
77
- const table = new Table({
78
- head: result.columns.map((column) => pc.cyan(column)),
79
- style: { head: [], border: [] },
80
- wordWrap: true
81
- });
82
- for (const row of shown) {
83
- table.push(result.columns.map((column) => formatCell(row[column])));
84
- }
85
- const lines = [table.toString()];
86
- if (truncate) {
87
- lines.push(
88
- pc.yellow(
89
- `\u2026 showing first ${MAX_ROWS_SHOWN} of ${result.rows.length} rows (add 'first N' or use \\export)`
90
- )
91
- );
92
- }
93
- lines.push(
94
- pc.dim(
95
- `${result.rows.length} row${result.rows.length === 1 ? "" : "s"} \xB7 ${result.durationMs}ms`
96
- )
97
- );
98
- return lines.join("\n");
99
- }
100
-
101
- export {
102
- plainTable,
103
- describeTable,
104
- formatTableList,
105
- formatResultSet
106
- };
File without changes