dbcat 0.0.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 ADDED
@@ -0,0 +1,61 @@
1
+ # dbcat
2
+
3
+ A simple CLI to view database tables. Supports PostgreSQL, MySQL, and SQLite.
4
+
5
+ ```sh
6
+ bunx dbcat ./data.db
7
+ ```
8
+
9
+ ## Usage
10
+
11
+ Connect to a database to browse all tables:
12
+
13
+ ```sh
14
+ # SQLite file
15
+ bunx dbcat ./database.sqlite
16
+
17
+ # PostgreSQL
18
+ bunx dbcat postgres://user:pass@localhost:5432/mydb
19
+
20
+ # MySQL
21
+ bunx dbcat mysql://user:pass@localhost:3306/mydb
22
+
23
+ # No argument - uses DATABASE_URL environment variable
24
+ bunx dbcat
25
+ ```
26
+
27
+ Run a query by piping SQL:
28
+
29
+ ```sh
30
+ echo "SELECT * FROM users WHERE active = true" | bunx dbcat ./data.db
31
+ ```
32
+
33
+ ### Options
34
+
35
+ | Flag | Description |
36
+ |------|-------------|
37
+ | `--full`, `-f` | Show all rows when browsing tables (default: 100) |
38
+ | `--json` | Output as JSON (indented if TTY) |
39
+ | `--json=color` | Output as normal object console.log |
40
+
41
+ Piped queries always return all rows.
42
+
43
+ ## Example
44
+
45
+ ```sh
46
+ $ bunx dbcat ./demo.sqlite
47
+ Connected to demo.sqlite
48
+
49
+ ╭─ users ────────────┬───────────────────┬─────────────────────╮
50
+ │ id │ name │ email │ created_at │
51
+ ├────┼───────────────┼───────────────────┼─────────────────────┤
52
+ │ 1 │ Alice Johnson │ alice@example.com │ 2025-12-08 05:25:21 │
53
+ │ 2 │ Bob Smith │ bob@example.com │ 2025-12-08 05:25:21 │
54
+ │ 3 │ Carol White │ carol@example.com │ 2025-12-08 05:25:21 │
55
+ │ ... 47 more rows │
56
+ ╰──────────────────────────────────────────────────────────────╯
57
+ ```
58
+
59
+ ## Requirements
60
+
61
+ [Bun](https://bun.sh) v1.3+
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "dbcat",
3
+ "version": "0.0.1",
4
+ "description": "A simple CLI to view database tables. Supports PostgreSQL, MySQL, and SQLite.",
5
+ "author": "RiskyMH",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/RiskyMH/dbcat"
10
+ },
11
+ "homepage": "https://github.com/RiskyMH/dbcat#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/RiskyMH/dbcat/issues"
14
+ },
15
+ "keywords": [
16
+ "database",
17
+ "cli",
18
+ "postgresql",
19
+ "mysql",
20
+ "sqlite",
21
+ "sql",
22
+ "bun"
23
+ ],
24
+ "module": "src/index.ts",
25
+ "bin": "src/index.ts",
26
+ "type": "module",
27
+ "files": [
28
+ "src/*"
29
+ ],
30
+ "devDependencies": {
31
+ "@types/bun": "latest",
32
+ "typescript": "^5"
33
+ }
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { SQL } from "bun";
4
+ import { printTable } from "./table.ts";
5
+
6
+ if (typeof Bun !== "object") throw new Error("Please install & use bun!");
7
+
8
+ export function createConnection(input?: string): InstanceType<typeof SQL> {
9
+ if (!input) {
10
+ return new SQL();
11
+ }
12
+
13
+ if (
14
+ !input.includes("://") &&
15
+ (input.endsWith(".db") ||
16
+ input.endsWith(".sqlite") ||
17
+ input.endsWith(".sqlite3"))
18
+ ) {
19
+ return new SQL(`sqlite://${input}`);
20
+ }
21
+
22
+ return new SQL(input);
23
+ }
24
+
25
+ export async function getDatabaseName(
26
+ sql: InstanceType<typeof SQL>
27
+ ): Promise<string | null> {
28
+ const adapter = sql.options.adapter;
29
+
30
+ switch (adapter) {
31
+ case "postgres": {
32
+ const result = await sql`SELECT current_database() as name`;
33
+ return result[0]?.name ?? null;
34
+ }
35
+ case "mysql":
36
+ case "mariadb": {
37
+ const result = await sql`SELECT DATABASE() as name`;
38
+ return result[0]?.name ?? null;
39
+ }
40
+ case "sqlite": {
41
+ const filename = (sql.options as { filename?: string }).filename;
42
+ if (!filename || filename === ":memory:") return null;
43
+
44
+ return filename.split("/").pop() ?? null;
45
+ }
46
+ default:
47
+ return null;
48
+ }
49
+ }
50
+
51
+ export function getAllTables(sql: InstanceType<typeof SQL>) {
52
+ const adapter = sql.options.adapter;
53
+
54
+ switch (adapter) {
55
+ case "postgres":
56
+ return sql`
57
+ SELECT table_name as name
58
+ FROM information_schema.tables
59
+ WHERE table_schema = 'public'
60
+ AND table_type = 'BASE TABLE'
61
+ ORDER BY table_name
62
+ ` as Promise<{ name: string }[]>;
63
+ case "mysql":
64
+ case "mariadb":
65
+ return sql`
66
+ SELECT table_name as name
67
+ FROM information_schema.tables
68
+ WHERE table_schema = DATABASE()
69
+ AND table_type = 'BASE TABLE'
70
+ ORDER BY table_name
71
+ ` as Promise<{ name: string }[]>;
72
+ case "sqlite":
73
+ return sql`
74
+ SELECT name
75
+ FROM sqlite_master
76
+ WHERE type = 'table'
77
+ AND name NOT LIKE 'sqlite_%'
78
+ ORDER BY name
79
+ ` as Promise<{ name: string }[]>;
80
+ default:
81
+ throw new Error(`Unsupported adapter: ${adapter}`);
82
+ }
83
+ }
84
+
85
+ export async function getTableCount(
86
+ sql: InstanceType<typeof SQL>,
87
+ tableName: string
88
+ ): Promise<number> {
89
+ const result = await sql`SELECT COUNT(*) as count FROM ${sql(tableName)}`;
90
+ return Number(result[0]?.count ?? 0);
91
+ }
92
+
93
+ export function getTableData(
94
+ sql: InstanceType<typeof SQL>,
95
+ tableName: string,
96
+ limit?: number
97
+ ) {
98
+ if (limit === undefined) {
99
+ return sql`SELECT * FROM ${sql(tableName)}` as Promise<
100
+ Record<string, unknown>[]
101
+ >;
102
+ }
103
+ return sql`SELECT * FROM ${sql(tableName)} LIMIT ${limit}` as Promise<
104
+ Record<string, unknown>[]
105
+ >;
106
+ }
107
+
108
+ export function runQuery(sql: InstanceType<typeof SQL>, query: string) {
109
+ return sql.unsafe(query) as Promise<Record<string, unknown>[]>;
110
+ }
111
+
112
+ export function formatTableOutput(
113
+ tableName: string,
114
+ rows: Record<string, unknown>[]
115
+ ): string {
116
+ const lines: string[] = [];
117
+ lines.push(`\n=== ${tableName} ===`);
118
+
119
+ if (rows.length === 0) {
120
+ lines.push("(empty table)");
121
+ return lines.join("\n");
122
+ }
123
+
124
+ const maxRows = 100;
125
+ const displayRows = rows.slice(0, maxRows);
126
+
127
+ if (displayRows.length > 0) {
128
+ const keys = Object.keys(displayRows[0]!);
129
+ lines.push(keys.join(" | "));
130
+ lines.push("-".repeat(keys.join(" | ").length));
131
+ for (const row of displayRows) {
132
+ lines.push(keys.map((k) => String(row[k] ?? "NULL")).join(" | "));
133
+ }
134
+ }
135
+
136
+ if (rows.length > maxRows) {
137
+ lines.push(`... and ${rows.length - maxRows} more rows`);
138
+ }
139
+
140
+ return lines.join("\n");
141
+ }
142
+
143
+ export function formatQueryResult(rows: Record<string, unknown>[]): string {
144
+ if (rows.length === 0) {
145
+ return "(no results)";
146
+ }
147
+
148
+ const lines: string[] = [];
149
+ const keys = Object.keys(rows[0]!);
150
+ lines.push(keys.join(" | "));
151
+ lines.push("-".repeat(keys.join(" | ").length));
152
+ for (const row of rows) {
153
+ lines.push(keys.map((k) => String(row[k] ?? "NULL")).join(" | "));
154
+ }
155
+ lines.push(`\n${rows.length} row(s)`);
156
+
157
+ return lines.join("\n");
158
+ }
159
+
160
+ function displayTable(
161
+ tableName: string,
162
+ rows: Record<string, unknown>[],
163
+ totalRows?: number,
164
+ maxRows?: number
165
+ ) {
166
+ console.log();
167
+ printTable(rows, { title: tableName, totalRows, maxRows });
168
+ }
169
+
170
+ function displayQueryResult(rows: Record<string, unknown>[]) {
171
+ printTable(rows, { title: "Result", maxRows: Infinity });
172
+ }
173
+
174
+ async function readStdin(): Promise<string | null> {
175
+ if (process.stdin.isTTY) {
176
+ return null;
177
+ }
178
+
179
+ try {
180
+ const text = await Bun.stdin.text();
181
+ return text.trim() || null;
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+
187
+ type JsonMode = false | "plain" | "color";
188
+
189
+ function parseArgs() {
190
+ const args = process.argv.slice(2);
191
+ let input: string | undefined;
192
+ let full = false;
193
+ let json: JsonMode = false;
194
+
195
+ for (const arg of args) {
196
+ if (arg === "--full" || arg === "-f") {
197
+ full = true;
198
+ } else if (arg === "--json") {
199
+ json = "plain";
200
+ } else if (arg === "--json=color") {
201
+ json = "color";
202
+ } else if (!arg.startsWith("-")) {
203
+ input = arg;
204
+ }
205
+ }
206
+
207
+ return { input, full, json };
208
+ }
209
+
210
+ const DEFAULT_LIMIT = 100;
211
+
212
+ function showUsageAndExit(): never {
213
+ console.error("Usage: dbcli <database>");
214
+ console.error("");
215
+ console.error("Examples:");
216
+ console.error(" dbcli ./data.db");
217
+ console.error(" dbcli postgres://user:pass@localhost/mydb");
218
+ console.error(" dbcli mysql://user:pass@localhost/mydb");
219
+ console.error("");
220
+ console.error("Or set DATABASE_URL environment variable.");
221
+ process.exit(1);
222
+ }
223
+
224
+ function outputJson(data: unknown, color: boolean) {
225
+ if (color) {
226
+ console.log(Bun.inspect(data, { colors: true, depth: Infinity }));
227
+ } else {
228
+ const indent = process.stdout.isTTY ? 2 : undefined;
229
+ console.log(JSON.stringify(data, null, indent));
230
+ }
231
+ }
232
+
233
+ async function main() {
234
+ const { input, full, json } = parseArgs();
235
+
236
+ if (!input && !process.env.DATABASE_URL) {
237
+ showUsageAndExit();
238
+ }
239
+
240
+ let sql: InstanceType<typeof SQL>;
241
+ try {
242
+ sql = createConnection(input);
243
+ } catch (error) {
244
+ console.error("Failed to connect:");
245
+ console.error(error instanceof Error ? error.message : String(error));
246
+ process.exit(1);
247
+ }
248
+
249
+ const isTTY = process.stdout.isTTY;
250
+ const dim = Bun.enableANSIColors ? "\x1b[2m" : "";
251
+ const reset = Bun.enableANSIColors ? "\x1b[0m" : "";
252
+ const bold = Bun.enableANSIColors ? "\x1b[1m" : "";
253
+ const clearLine = "\x1b[2K\r";
254
+
255
+ try {
256
+ const stdinQuery = await readStdin();
257
+
258
+ if (stdinQuery) {
259
+ const results = await runQuery(sql, stdinQuery);
260
+ if (json) {
261
+ outputJson(results, json === "color");
262
+ } else {
263
+ displayQueryResult(results);
264
+ }
265
+ } else {
266
+ if (isTTY && !json) {
267
+ process.stdout.write(
268
+ `${dim}Connecting to ${sql.options.adapter}...${reset}`
269
+ );
270
+ }
271
+
272
+ const [dbName, tables] = await Promise.all([
273
+ getDatabaseName(sql),
274
+ getAllTables(sql),
275
+ ]);
276
+
277
+ if (json) {
278
+ const result: Record<string, unknown[]> = {};
279
+ for (const table of tables) {
280
+ const data = full
281
+ ? await getTableData(sql, table.name)
282
+ : await getTableData(sql, table.name, DEFAULT_LIMIT);
283
+ result[table.name] = data;
284
+ }
285
+ outputJson(result, json === "color");
286
+ } else {
287
+ const dbDisplay = dbName
288
+ ? `${bold}${dbName}${reset}`
289
+ : `${bold}${sql.options.adapter}${reset} ${dim}database${reset}`;
290
+ if (isTTY) {
291
+ process.stdout.write(
292
+ `${clearLine}${dim}Connected to${reset} ${dbDisplay}\n`
293
+ );
294
+ } else {
295
+ console.log(`Connected to ${dbName || sql.options.adapter}`);
296
+ }
297
+
298
+ if (tables.length === 0) {
299
+ console.log(`${dim}No tables found.${reset}`);
300
+ } else {
301
+ for (const table of tables) {
302
+ if (full) {
303
+ const data = await getTableData(sql, table.name);
304
+ displayTable(table.name, data, undefined, Infinity);
305
+ } else {
306
+ const [count, data] = await Promise.all([
307
+ getTableCount(sql, table.name),
308
+ getTableData(sql, table.name, DEFAULT_LIMIT),
309
+ ]);
310
+ displayTable(table.name, data, count, DEFAULT_LIMIT);
311
+ }
312
+ }
313
+ }
314
+ }
315
+ }
316
+ } catch (error) {
317
+ if (isTTY && !json) {
318
+ process.stdout.write(clearLine);
319
+ }
320
+ const message = error instanceof Error ? error.message : String(error);
321
+
322
+ if (
323
+ message.includes("Connection") ||
324
+ message.includes("connect") ||
325
+ message.includes("ECONNREFUSED")
326
+ ) {
327
+ console.error(`Failed to connect to ${sql.options.adapter}:`);
328
+ } else {
329
+ console.error("Error:");
330
+ }
331
+ console.error(message);
332
+ process.exit(1);
333
+ } finally {
334
+ await sql.close();
335
+ }
336
+ }
337
+
338
+ if (import.meta.main) {
339
+ main();
340
+ }
package/src/table.ts ADDED
@@ -0,0 +1,297 @@
1
+ const RESET = "\x1b[0m";
2
+ const DIM = "\x1b[2m";
3
+ const BOLD = "\x1b[1m";
4
+
5
+ const BOX = {
6
+ topLeft: "╭",
7
+ topRight: "╮",
8
+ bottomLeft: "╰",
9
+ bottomRight: "╯",
10
+ horizontal: "─",
11
+ vertical: "│",
12
+ headerLeft: "├",
13
+ headerRight: "┤",
14
+ headerCross: "┼",
15
+ topCross: "┬",
16
+ bottomCross: "┴",
17
+ };
18
+
19
+ function getTerminalWidth(): number {
20
+ return process.stdout.columns || 120;
21
+ }
22
+
23
+ function formatValue(value: unknown): string {
24
+ if (value === null || value === undefined) {
25
+ const dim = Bun.enableANSIColors ? DIM : "";
26
+ const reset = Bun.enableANSIColors ? RESET : "";
27
+ return `${dim}NULL${reset}`;
28
+ }
29
+ if (typeof value === "string") {
30
+ return value;
31
+ }
32
+ return Bun.inspect(value, { colors: Bun.enableANSIColors, depth: 2 });
33
+ }
34
+
35
+ function truncate(str: string, maxWidth: number): string {
36
+ const width = Bun.stringWidth(str);
37
+ if (width <= maxWidth) {
38
+ return str;
39
+ }
40
+
41
+ let low = 0;
42
+ let high = str.length;
43
+
44
+ while (low < high) {
45
+ const mid = Math.floor((low + high + 1) / 2);
46
+ if (Bun.stringWidth(str.slice(0, mid)) + 1 <= maxWidth) {
47
+ low = mid;
48
+ } else {
49
+ high = mid - 1;
50
+ }
51
+ }
52
+
53
+ const truncated = str.slice(0, low) + "…";
54
+ return Bun.enableANSIColors ? truncated + RESET : truncated;
55
+ }
56
+
57
+ function padRight(str: string, width: number): string {
58
+ const strWidth = Bun.stringWidth(str);
59
+ if (strWidth >= width) {
60
+ return str;
61
+ }
62
+ return str + " ".repeat(width - strWidth);
63
+ }
64
+
65
+ function padLeft(str: string, width: number): string {
66
+ const strWidth = Bun.stringWidth(str);
67
+ if (strWidth >= width) {
68
+ return str;
69
+ }
70
+ return " ".repeat(width - strWidth) + str;
71
+ }
72
+
73
+ export interface TableOptions {
74
+ maxRows?: number;
75
+ title?: string;
76
+ totalRows?: number;
77
+ }
78
+
79
+ export function printTable(
80
+ rows: Record<string, unknown>[],
81
+ options: TableOptions = {}
82
+ ): void {
83
+ const { maxRows = 100, title, totalRows } = options;
84
+ const dim = Bun.enableANSIColors ? DIM : "";
85
+ const reset = Bun.enableANSIColors ? RESET : "";
86
+ const bold = Bun.enableANSIColors ? BOLD : "";
87
+
88
+ if (rows.length === 0) {
89
+ if (title) {
90
+ const contentWidth = Math.max(
91
+ Bun.stringWidth("(empty)"),
92
+ Bun.stringWidth(title)
93
+ );
94
+ const innerWidth = contentWidth + 2;
95
+ const titleDisplay = ` ${title} `;
96
+ const titleWidth = Bun.stringWidth(titleDisplay);
97
+ const remainingWidth = innerWidth - titleWidth;
98
+ console.log(
99
+ `${dim}${BOX.topLeft}${
100
+ BOX.horizontal
101
+ }${reset}${bold}${titleDisplay}${reset}${dim}${BOX.horizontal.repeat(
102
+ Math.max(0, remainingWidth)
103
+ )}${BOX.topRight}${reset}`
104
+ );
105
+ console.log(
106
+ `${dim}${BOX.vertical}${reset} ${padRight(
107
+ "(empty)",
108
+ contentWidth
109
+ )} ${dim}${BOX.vertical}${reset}`
110
+ );
111
+ console.log(
112
+ `${dim}${BOX.bottomLeft}${BOX.horizontal.repeat(innerWidth)}${
113
+ BOX.bottomRight
114
+ }${reset}`
115
+ );
116
+ } else {
117
+ console.log("(empty)");
118
+ }
119
+ return;
120
+ }
121
+
122
+ const rowCount = rows.length;
123
+ const displayRows = maxRows === Infinity ? rows : rows.slice(0, maxRows);
124
+ const allColumns = Object.keys(displayRows[0]!);
125
+ const termWidth = getTerminalWidth();
126
+
127
+ const isNumericCol: boolean[] = allColumns.map((col) => {
128
+ return displayRows.every((row) => {
129
+ const val = row[col];
130
+ return (
131
+ val === null ||
132
+ val === undefined ||
133
+ typeof val === "number" ||
134
+ typeof val === "bigint"
135
+ );
136
+ });
137
+ });
138
+
139
+ const colWidths: number[] = allColumns.map((col) => Bun.stringWidth(col));
140
+ const formattedRows: string[][] = displayRows.map((row) =>
141
+ allColumns.map((col, i) => {
142
+ const formatted = formatValue(row[col]).replace(/\n/g, " ");
143
+ colWidths[i] = Math.max(colWidths[i]!, Bun.stringWidth(formatted));
144
+ return formatted;
145
+ })
146
+ );
147
+
148
+ const minColWidth = 3;
149
+ const borderOverhead = 4 + (allColumns.length - 1) * 3;
150
+ let availableForColumns = termWidth - borderOverhead;
151
+
152
+ let columns = allColumns;
153
+ let hiddenCols = 0;
154
+
155
+ while (
156
+ columns.length * minColWidth > availableForColumns &&
157
+ columns.length > 1
158
+ ) {
159
+ columns = columns.slice(0, -1);
160
+ hiddenCols++;
161
+ availableForColumns = termWidth - (4 + (columns.length - 1) * 3);
162
+ }
163
+
164
+ const visibleColWidths = colWidths.slice(0, columns.length);
165
+ const visibleFormattedRows = formattedRows.map((row) =>
166
+ row.slice(0, columns.length)
167
+ );
168
+ const visibleIsNumeric = isNumericCol.slice(0, columns.length);
169
+
170
+ let totalWidth = visibleColWidths.reduce((a, b) => a + b, 0);
171
+
172
+ if (totalWidth > availableForColumns) {
173
+ const headerWidths = columns.map((col) =>
174
+ Math.max(minColWidth, Bun.stringWidth(col))
175
+ );
176
+ const sqrtWidths = visibleColWidths.map((w) => Math.sqrt(w));
177
+ const sqrtTotal = sqrtWidths.reduce((a, b) => a + b, 0);
178
+
179
+ for (let i = 0; i < visibleColWidths.length; i++) {
180
+ const fair = Math.floor(
181
+ (sqrtWidths[i]! / sqrtTotal) * availableForColumns
182
+ );
183
+ visibleColWidths[i] = Math.max(headerWidths[i]!, fair);
184
+ }
185
+
186
+ totalWidth = visibleColWidths.reduce((a, b) => a + b, 0);
187
+ while (
188
+ totalWidth > availableForColumns &&
189
+ visibleColWidths.some((w) => w > minColWidth)
190
+ ) {
191
+ let maxIdx = 0;
192
+ for (let i = 1; i < visibleColWidths.length; i++) {
193
+ if (visibleColWidths[i]! > visibleColWidths[maxIdx]!) maxIdx = i;
194
+ }
195
+ visibleColWidths[maxIdx]!--;
196
+ totalWidth--;
197
+ }
198
+ }
199
+
200
+ const actualTotal = totalRows ?? rowCount;
201
+ const truncatedRows =
202
+ actualTotal > displayRows.length ? actualTotal - displayRows.length : 0;
203
+ let infoText = "";
204
+ if (truncatedRows > 0 || hiddenCols > 0) {
205
+ const parts: string[] = [];
206
+ if (truncatedRows > 0)
207
+ parts.push(`${truncatedRows} more row${truncatedRows > 1 ? "s" : ""}`);
208
+ if (hiddenCols > 0)
209
+ parts.push(`${hiddenCols} more column${hiddenCols > 1 ? "s" : ""}`);
210
+ infoText = `... ${parts.join(", ")}`;
211
+ }
212
+
213
+ const contentWidth =
214
+ visibleColWidths.reduce((a, b) => a + b, 0) + (columns.length - 1) * 3;
215
+
216
+ const titleWidth = title ? Bun.stringWidth(title) + 4 : 0;
217
+ const maxInnerWidth = termWidth - 4;
218
+ const innerWidth = Math.min(
219
+ Math.max(contentWidth, titleWidth),
220
+ maxInnerWidth
221
+ );
222
+
223
+ const totalInnerWidth = innerWidth + 2;
224
+
225
+ if (innerWidth > contentWidth && visibleColWidths.length > 0) {
226
+ visibleColWidths[visibleColWidths.length - 1]! += innerWidth - contentWidth;
227
+ }
228
+
229
+ const topBorder = visibleColWidths
230
+ .map((w) => BOX.horizontal.repeat(w))
231
+ .join(`${BOX.horizontal}${BOX.topCross}${BOX.horizontal}`);
232
+ const fullTopBorder = `${BOX.topLeft}${BOX.horizontal}${topBorder}${BOX.horizontal}${BOX.topRight}`;
233
+ if (title) {
234
+ const titleDisplay = ` ${title} `;
235
+ const titleDisplayWidth = Bun.stringWidth(titleDisplay);
236
+
237
+ const beforeTitle = fullTopBorder.slice(0, 2);
238
+ const afterTitle = fullTopBorder.slice(2 + titleDisplayWidth);
239
+ console.log(
240
+ `${dim}${beforeTitle}${reset}${bold}${titleDisplay}${reset}${dim}${afterTitle}${reset}`
241
+ );
242
+ } else {
243
+ console.log(`${dim}${fullTopBorder}${reset}`);
244
+ }
245
+
246
+ const header = columns
247
+ .map((col, i) => {
248
+ const truncated = truncate(col, visibleColWidths[i]!);
249
+ return visibleIsNumeric[i]
250
+ ? padLeft(truncated, visibleColWidths[i]!)
251
+ : padRight(truncated, visibleColWidths[i]!);
252
+ })
253
+ .join(` ${dim}${BOX.vertical}${reset} `);
254
+ console.log(
255
+ `${dim}${BOX.vertical}${reset} ${header} ${dim}${BOX.vertical}${reset}`
256
+ );
257
+
258
+ const headerSep = visibleColWidths
259
+ .map((w) => BOX.horizontal.repeat(w))
260
+ .join(`${BOX.horizontal}${BOX.headerCross}${BOX.horizontal}`);
261
+ console.log(
262
+ `${dim}${BOX.headerLeft}${BOX.horizontal}${headerSep}${BOX.horizontal}${BOX.headerRight}${reset}`
263
+ );
264
+
265
+ for (const row of visibleFormattedRows) {
266
+ const line = row
267
+ .map((val, i) => {
268
+ const truncated = truncate(val, visibleColWidths[i]!);
269
+ return visibleIsNumeric[i]
270
+ ? padLeft(truncated, visibleColWidths[i]!)
271
+ : padRight(truncated, visibleColWidths[i]!);
272
+ })
273
+ .join(` ${dim}${BOX.vertical}${reset} `);
274
+ console.log(
275
+ `${dim}${BOX.vertical}${reset} ${line} ${dim}${BOX.vertical}${reset}`
276
+ );
277
+ }
278
+
279
+ if (infoText) {
280
+ const truncatedInfo = truncate(infoText, innerWidth);
281
+ const infoLine = padRight(truncatedInfo, innerWidth);
282
+ console.log(`${dim}${BOX.vertical} ${infoLine} ${BOX.vertical}${reset}`);
283
+
284
+ console.log(
285
+ `${dim}${BOX.bottomLeft}${BOX.horizontal.repeat(totalInnerWidth)}${
286
+ BOX.bottomRight
287
+ }${reset}`
288
+ );
289
+ } else {
290
+ const bottomBorder = visibleColWidths
291
+ .map((w) => BOX.horizontal.repeat(w))
292
+ .join(`${BOX.horizontal}${BOX.bottomCross}${BOX.horizontal}`);
293
+ console.log(
294
+ `${dim}${BOX.bottomLeft}${BOX.horizontal}${bottomBorder}${BOX.horizontal}${BOX.bottomRight}${reset}`
295
+ );
296
+ }
297
+ }