@luckymingxuan/dbcli 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mingxuan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # dbcli
2
+ A personal database CLI giving AI full access to autonomously create, read, and store data.
@@ -0,0 +1,23 @@
1
+ interface ConnectionInfo {
2
+ url: string;
3
+ database: string;
4
+ host: string;
5
+ port: number;
6
+ username: string;
7
+ password: string;
8
+ lastConnected: string;
9
+ enabled: boolean;
10
+ }
11
+ export declare function getConnections(): Map<string, ConnectionInfo>;
12
+ export declare function getActiveConnection(): Promise<ConnectionInfo | null>;
13
+ export declare function getConnectionByName(name: string): Promise<ConnectionInfo | null>;
14
+ export declare function showConnections(): Promise<void>;
15
+ export declare function deleteConnection(name: string): Promise<void>;
16
+ export declare function connect(nameUrl: string, options?: {
17
+ username?: string;
18
+ password?: string;
19
+ current?: boolean;
20
+ }): Promise<void>;
21
+ export declare function disconnect(name: string): Promise<void>;
22
+ export declare function logout(name: string): Promise<void>;
23
+ export {};
@@ -0,0 +1,220 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { PostgresDriver } from '../drivers/postgres.js';
4
+ import { promises as fs } from 'fs';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ const CONFIG_DIR = path.join(os.homedir(), '.dbcli');
8
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'connections.json');
9
+ let connections = new Map();
10
+ async function loadConnections() {
11
+ try {
12
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
13
+ const data = await fs.readFile(CONFIG_FILE, 'utf-8');
14
+ const parsed = JSON.parse(data);
15
+ connections = new Map(Object.entries(parsed));
16
+ }
17
+ catch {
18
+ connections = new Map();
19
+ }
20
+ }
21
+ async function saveConnections() {
22
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
23
+ const obj = Object.fromEntries(connections);
24
+ await fs.writeFile(CONFIG_FILE, JSON.stringify(obj, null, 2), 'utf-8');
25
+ }
26
+ export function getConnections() {
27
+ return connections;
28
+ }
29
+ export async function getActiveConnection() {
30
+ await loadConnections();
31
+ for (const conn of connections.values()) {
32
+ if (conn.enabled) {
33
+ return conn;
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ export async function getConnectionByName(name) {
39
+ await loadConnections();
40
+ return connections.get(name) || null;
41
+ }
42
+ async function promptCredentials() {
43
+ const answers = await inquirer.prompt([
44
+ {
45
+ type: 'input',
46
+ name: 'username',
47
+ message: 'Username:',
48
+ default: 'postgres',
49
+ },
50
+ {
51
+ type: 'password',
52
+ name: 'password',
53
+ message: 'Password:',
54
+ mask: '*',
55
+ },
56
+ ]);
57
+ return answers;
58
+ }
59
+ function isUrl(str) {
60
+ return str.includes('://');
61
+ }
62
+ function buildUrl(info, username, password) {
63
+ return `postgresql://${username}:${password}@${info.host}:${info.port}/${info.database}`;
64
+ }
65
+ export async function showConnections() {
66
+ await loadConnections();
67
+ if (connections.size === 0) {
68
+ console.log(chalk.yellow('No saved connections.'));
69
+ console.log(chalk.cyan('Use "dbcli connect <url>" to add a new connection.'));
70
+ return;
71
+ }
72
+ const activeConnection = Array.from(connections.values()).find((conn) => conn.enabled);
73
+ if (activeConnection) {
74
+ console.log(chalk.green(`Current connected database: "${activeConnection.database}" (user: ${activeConnection.username})`));
75
+ }
76
+ else {
77
+ console.log(chalk.yellow('No database is currently connected.'));
78
+ }
79
+ console.log(chalk.cyan('Saved connections:'));
80
+ const rows = Array.from(connections.entries()).map(([name, info]) => ({
81
+ username: info.username || '-',
82
+ database: info.database,
83
+ host: info.host,
84
+ port: info.port,
85
+ status: info.enabled ? 'enabled' : 'disabled',
86
+ lastConnected: info.lastConnected,
87
+ }));
88
+ console.table(rows);
89
+ }
90
+ export async function deleteConnection(name) {
91
+ await loadConnections();
92
+ if (!connections.has(name)) {
93
+ console.error(chalk.red(`Connection "${name}" not found.`));
94
+ process.exit(1);
95
+ }
96
+ connections.delete(name);
97
+ await saveConnections();
98
+ console.log(chalk.yellow(`Connection "${name}" deleted.`));
99
+ }
100
+ export async function connect(nameUrl, options) {
101
+ await loadConnections();
102
+ let database;
103
+ let host;
104
+ let port;
105
+ let existing = null;
106
+ let urlUsername = '';
107
+ let urlPassword = '';
108
+ if (isUrl(nameUrl)) {
109
+ const url = new URL(nameUrl);
110
+ database = url.pathname.slice(1) || 'postgres';
111
+ host = url.hostname;
112
+ port = parseInt(url.port) || 5432;
113
+ urlUsername = url.username;
114
+ urlPassword = url.password;
115
+ existing = connections.get(database) || null;
116
+ }
117
+ else {
118
+ database = nameUrl;
119
+ existing = connections.get(database) || null;
120
+ if (!existing) {
121
+ console.error(chalk.red(`Connection "${database}" not found.`));
122
+ console.log(chalk.cyan('Use "dbcli connect <url>" to add a new connection.'));
123
+ process.exit(1);
124
+ }
125
+ host = existing.host;
126
+ port = existing.port;
127
+ }
128
+ let credentials;
129
+ if (options?.current) {
130
+ const activeConn = Array.from(connections.values()).find((conn) => conn.enabled);
131
+ if (!activeConn) {
132
+ console.error(chalk.red('No currently connected database found.'));
133
+ process.exit(1);
134
+ }
135
+ credentials = { username: activeConn.username, password: activeConn.password };
136
+ }
137
+ else if (options?.username && options?.password) {
138
+ credentials = { username: options.username, password: options.password };
139
+ }
140
+ else if (urlUsername || urlPassword) {
141
+ credentials = { username: urlUsername, password: urlPassword };
142
+ }
143
+ else if (existing && existing.username && existing.password) {
144
+ const useExisting = await inquirer.prompt([
145
+ {
146
+ type: 'input',
147
+ name: 'value',
148
+ message: `You have an existing account "${existing.username}". Enter "y" to use it or anything else to re-login:`,
149
+ default: 'y',
150
+ },
151
+ ]);
152
+ if (useExisting.value.toLowerCase() === 'y') {
153
+ credentials = { username: existing.username, password: existing.password };
154
+ }
155
+ else {
156
+ credentials = await promptCredentials();
157
+ }
158
+ }
159
+ else {
160
+ credentials = await promptCredentials();
161
+ }
162
+ let finalUrl;
163
+ if (existing) {
164
+ finalUrl = buildUrl(existing, credentials.username, credentials.password);
165
+ }
166
+ else {
167
+ finalUrl = `postgresql://${credentials.username}:${credentials.password}@${host}:${port}/${database}`;
168
+ }
169
+ const driver = new PostgresDriver();
170
+ try {
171
+ console.log(chalk.cyan(`Connecting to db("${database}")...`));
172
+ await driver.connect(finalUrl);
173
+ for (const conn of connections.values()) {
174
+ conn.enabled = false;
175
+ }
176
+ const connectionInfo = {
177
+ url: `postgresql://${host}:${port}/${database}`,
178
+ database,
179
+ host,
180
+ port,
181
+ username: credentials.username,
182
+ password: credentials.password,
183
+ lastConnected: new Date().toISOString(),
184
+ enabled: true,
185
+ };
186
+ connections.set(database, connectionInfo);
187
+ await saveConnections();
188
+ console.log(chalk.green(`Connected to db("${database}") as user("${credentials.username}") successfully!`));
189
+ process.exit(0);
190
+ }
191
+ catch (error) {
192
+ const message = error instanceof Error ? error.message : String(error);
193
+ console.error(chalk.red(`Connection failed: ${message}`));
194
+ process.exit(1);
195
+ }
196
+ }
197
+ export async function disconnect(name) {
198
+ await loadConnections();
199
+ if (!connections.has(name)) {
200
+ console.error(chalk.red(`Connection "${name}" not found.`));
201
+ process.exit(1);
202
+ }
203
+ const conn = connections.get(name);
204
+ conn.enabled = false;
205
+ await saveConnections();
206
+ console.log(chalk.yellow(`Disconnected from "${name}".`));
207
+ }
208
+ export async function logout(name) {
209
+ await loadConnections();
210
+ if (!connections.has(name)) {
211
+ console.error(chalk.red(`Connection "${name}" not found.`));
212
+ process.exit(1);
213
+ }
214
+ const conn = connections.get(name);
215
+ conn.enabled = false;
216
+ conn.username = '';
217
+ conn.password = '';
218
+ await saveConnections();
219
+ console.log(chalk.yellow(`Logged out from "${name}". Username and password cleared.`));
220
+ }
@@ -0,0 +1 @@
1
+ export declare function executeQuery(sql: string): Promise<void>;
@@ -0,0 +1,45 @@
1
+ import chalk from 'chalk';
2
+ import { PostgresDriver } from '../drivers/postgres.js';
3
+ import { getActiveConnection } from './connect.js';
4
+ async function getActiveDriver() {
5
+ const activeConn = await getActiveConnection();
6
+ if (!activeConn) {
7
+ return null;
8
+ }
9
+ const url = `postgresql://${activeConn.username}:${activeConn.password}@${activeConn.host}:${activeConn.port}/${activeConn.database}`;
10
+ const driver = new PostgresDriver();
11
+ await driver.connect(url);
12
+ const connName = activeConn.database;
13
+ return { driver, connName };
14
+ }
15
+ export async function executeQuery(sql) {
16
+ const result = await getActiveDriver();
17
+ if (!result) {
18
+ console.log(chalk.red('No active database connection.'));
19
+ console.log(chalk.cyan('Please use "dbcli connect <url>" to connect to a database first.'));
20
+ process.exit(1);
21
+ }
22
+ const { driver, connName } = result;
23
+ try {
24
+ console.log(chalk.gray(`Executing on database "${connName}":`));
25
+ console.log(chalk.cyan(sql));
26
+ console.log();
27
+ const queryResult = await driver.query(sql);
28
+ if (queryResult.rows.length === 0) {
29
+ console.log(chalk.yellow(`Query executed successfully. No rows returned.`));
30
+ console.log(chalk.gray(`Row count: ${queryResult.rowCount}`));
31
+ }
32
+ else {
33
+ console.table(queryResult.rows);
34
+ console.log(chalk.gray(`Total: ${queryResult.rows.length} row(s)`));
35
+ }
36
+ }
37
+ catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ console.error(chalk.red(`Query failed: ${message}`));
40
+ process.exit(1);
41
+ }
42
+ finally {
43
+ await driver.disconnect();
44
+ }
45
+ }
@@ -0,0 +1,2 @@
1
+ export declare function listTables(): Promise<void>;
2
+ export declare function describeTable(tableName: string): Promise<void>;
@@ -0,0 +1,67 @@
1
+ import chalk from 'chalk';
2
+ import { PostgresDriver } from '../drivers/postgres.js';
3
+ import { getActiveConnection } from './connect.js';
4
+ async function getActiveDriver() {
5
+ const activeConn = await getActiveConnection();
6
+ if (!activeConn) {
7
+ return null;
8
+ }
9
+ const url = `postgresql://${activeConn.username}:${activeConn.password}@${activeConn.host}:${activeConn.port}/${activeConn.database}`;
10
+ const driver = new PostgresDriver();
11
+ await driver.connect(url);
12
+ const connName = activeConn.database;
13
+ return { driver, connName };
14
+ }
15
+ export async function listTables() {
16
+ const result = await getActiveDriver();
17
+ if (!result) {
18
+ console.log(chalk.red('No active database connection.'));
19
+ console.log(chalk.cyan('Please use "dbcli connect <url>" to connect to a database first.'));
20
+ process.exit(1);
21
+ }
22
+ const { driver, connName } = result;
23
+ try {
24
+ const tables = await driver.listTables();
25
+ if (tables.length === 0) {
26
+ console.log(chalk.yellow(`No tables found in database "${connName}".`));
27
+ }
28
+ else {
29
+ console.log(chalk.cyan(`Tables in database "${connName}":`));
30
+ console.table(tables);
31
+ }
32
+ }
33
+ finally {
34
+ await driver.disconnect();
35
+ }
36
+ }
37
+ export async function describeTable(tableName) {
38
+ const result = await getActiveDriver();
39
+ if (!result) {
40
+ console.log(chalk.red('No active database connection.'));
41
+ console.log(chalk.cyan('Please use "dbcli connect <url>" to connect to a database first.'));
42
+ process.exit(1);
43
+ }
44
+ const { driver, connName } = result;
45
+ try {
46
+ const columns = await driver.describeTable('public', tableName);
47
+ if (columns.length === 0) {
48
+ console.log(chalk.yellow(`Table "${tableName}" not found in database "${connName}".`));
49
+ }
50
+ else {
51
+ console.log(chalk.cyan(`Structure of table "${tableName}" in database "${connName}":`));
52
+ console.table(columns);
53
+ const hasDescriptions = columns.some(col => col.description);
54
+ if (hasDescriptions) {
55
+ console.log(chalk.gray('\nColumn descriptions:'));
56
+ columns.forEach(col => {
57
+ if (col.description) {
58
+ console.log(` ${chalk.white(col.name)}: ${chalk.gray(col.description)}`);
59
+ }
60
+ });
61
+ }
62
+ }
63
+ }
64
+ finally {
65
+ await driver.disconnect();
66
+ }
67
+ }
@@ -0,0 +1,28 @@
1
+ export interface QueryResult {
2
+ rows: Record<string, unknown>[];
3
+ rowCount: number;
4
+ fields: FieldInfo[];
5
+ }
6
+ export interface FieldInfo {
7
+ name: string;
8
+ dataTypeID: number;
9
+ }
10
+ export interface TableInfo {
11
+ schema: string;
12
+ name: string;
13
+ }
14
+ export interface ColumnInfo {
15
+ name: string;
16
+ dataType: string;
17
+ isNullable: boolean;
18
+ defaultValue: string | null;
19
+ description: string | null;
20
+ }
21
+ export interface DatabaseDriver {
22
+ connect(url: string): Promise<void>;
23
+ disconnect(): Promise<void>;
24
+ isConnected(): boolean;
25
+ query(sql: string): Promise<QueryResult>;
26
+ listTables(): Promise<TableInfo[]>;
27
+ describeTable(schema: string, table: string): Promise<ColumnInfo[]>;
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { DatabaseDriver, QueryResult, TableInfo, ColumnInfo } from './interface.js';
2
+ export declare class PostgresDriver implements DatabaseDriver {
3
+ private client;
4
+ private pool;
5
+ private connected;
6
+ connect(url: string): Promise<void>;
7
+ disconnect(): Promise<void>;
8
+ isConnected(): boolean;
9
+ query(sql: string, params?: unknown[]): Promise<QueryResult>;
10
+ listTables(): Promise<TableInfo[]>;
11
+ describeTable(schema: string, table: string): Promise<ColumnInfo[]>;
12
+ }
@@ -0,0 +1,86 @@
1
+ /*
2
+ * @Author: Mingxuan songmingxuan936@gmail.com
3
+ * @Date: 2026-04-05 17:01:59
4
+ * @LastEditors: Mingxuan songmingxuan936@gmail.com
5
+ * @LastEditTime: 2026-04-05 17:42:59
6
+ * @FilePath: /dbcli/src/drivers/postgres.ts
7
+ * @Description:
8
+ *
9
+ * Copyright (c) 2026 by ${git_name_email}, All Rights Reserved.
10
+ */
11
+ import pg from 'pg';
12
+ export class PostgresDriver {
13
+ client = null;
14
+ pool = null;
15
+ connected = false;
16
+ async connect(url) {
17
+ const connectionUrl = new URL(url);
18
+ this.pool = new pg.Pool({
19
+ host: connectionUrl.hostname,
20
+ port: parseInt(connectionUrl.port) || 5432,
21
+ user: connectionUrl.username,
22
+ password: connectionUrl.password,
23
+ database: connectionUrl.pathname.slice(1) || 'postgres',
24
+ ssl: connectionUrl.searchParams.get('sslmode') === 'require' ? { rejectUnauthorized: false } : undefined,
25
+ });
26
+ this.client = await this.pool.connect();
27
+ this.connected = true;
28
+ }
29
+ async disconnect() {
30
+ if (this.client) {
31
+ this.client.release();
32
+ this.client = null;
33
+ }
34
+ if (this.pool) {
35
+ await this.pool.end();
36
+ this.pool = null;
37
+ }
38
+ this.connected = false;
39
+ }
40
+ isConnected() {
41
+ return this.connected && this.client !== null;
42
+ }
43
+ async query(sql, params) {
44
+ if (!this.client) {
45
+ throw new Error('Not connected to database');
46
+ }
47
+ const result = await this.client.query(sql, params);
48
+ return {
49
+ rows: result.rows,
50
+ rowCount: result.rowCount ?? 0,
51
+ fields: result.fields.map((f) => ({
52
+ name: f.name,
53
+ dataTypeID: f.dataTypeID,
54
+ })),
55
+ };
56
+ }
57
+ async listTables() {
58
+ const result = await this.query(`
59
+ SELECT schemaname as schema, tablename as name
60
+ FROM pg_tables
61
+ WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
62
+ ORDER BY schemaname, tablename
63
+ `);
64
+ return result.rows;
65
+ }
66
+ async describeTable(schema, table) {
67
+ const result = await this.query(`
68
+ SELECT
69
+ a.attname as name,
70
+ format_type(a.atttypid, a.atttypmod) as data_type,
71
+ CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END as is_nullable,
72
+ pg_get_expr(ad.adbin, a.attrelid) as default_value,
73
+ col_description(a.attrelid, a.attnum) as description
74
+ FROM pg_attribute a
75
+ JOIN pg_class c ON c.oid = a.attrelid
76
+ JOIN pg_namespace n ON n.oid = c.relnamespace
77
+ LEFT JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
78
+ WHERE n.nspname = $1
79
+ AND c.relname = $2
80
+ AND a.attnum > 0
81
+ AND NOT a.attisdropped
82
+ ORDER BY a.attnum
83
+ `, [schema, table]);
84
+ return result.rows;
85
+ }
86
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,74 @@
1
+ import { Command } from 'commander';
2
+ import { connect, disconnect, logout, showConnections as statusConnections, deleteConnection } from './commands/connect.js';
3
+ import { listTables, describeTable } from './commands/tables.js';
4
+ import { executeQuery } from './commands/query.js';
5
+ const program = new Command();
6
+ program
7
+ .name('dbcli')
8
+ .description('Database CLI tool')
9
+ .version('0.1.0')
10
+ .addHelpCommand(false);
11
+ program
12
+ .command('connect')
13
+ .description('Connect to a database (-u <user> -p <pass> for credentials, -c to use current)')
14
+ .argument('<db-name|url>', 'Connection name (for existing) or full URL (for new connection)')
15
+ .option('-u, --username <username>', 'Username for authentication')
16
+ .option('-p, --password <password>', 'Password for authentication')
17
+ .option('-c, --current', 'Use credentials from currently connected database')
18
+ .action(async (nameUrl, options) => {
19
+ await connect(nameUrl, options);
20
+ });
21
+ program
22
+ .option('-s, --status', 'Show all saved connections status')
23
+ .action(async (options, cmd) => {
24
+ if (cmd.args && cmd.args.length > 0) {
25
+ console.error(`error: unknown command '${cmd.args[0]}'`);
26
+ process.exit(1);
27
+ }
28
+ if (options.status) {
29
+ await statusConnections();
30
+ process.exit(0);
31
+ }
32
+ });
33
+ program
34
+ .command('disconnect')
35
+ .description('Disconnect from a database (keeps credentials)')
36
+ .argument('<db-name>', 'Connection name to disconnect')
37
+ .action(async (name) => {
38
+ await disconnect(name);
39
+ });
40
+ program
41
+ .command('logout')
42
+ .description('Logout from a connection (clears credentials)')
43
+ .argument('<db-name>', 'Connection name to logout')
44
+ .action(async (name) => {
45
+ await logout(name);
46
+ });
47
+ program
48
+ .command('delete')
49
+ .description('Delete a saved connection completely')
50
+ .argument('<db-name>', 'Connection name to delete')
51
+ .action(async (name) => {
52
+ await deleteConnection(name);
53
+ });
54
+ program
55
+ .command('tables')
56
+ .description('List all tables in the connected database')
57
+ .action(async () => {
58
+ await listTables();
59
+ });
60
+ program
61
+ .command('describe')
62
+ .description('Show the structure of a table')
63
+ .argument('<table>', 'Table name to describe')
64
+ .action(async (table) => {
65
+ await describeTable(table);
66
+ });
67
+ program
68
+ .command('query')
69
+ .description('Execute a SQL query')
70
+ .argument('<sql>', 'SQL query to execute (use quotes for complex queries)')
71
+ .action(async (sql) => {
72
+ await executeQuery(sql);
73
+ });
74
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@luckymingxuan/dbcli",
3
+ "version": "0.1.0",
4
+ "description": "Database CLI tool",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "dbcli": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "tsx src/index.ts"
14
+ },
15
+ "dependencies": {
16
+ "chalk": "^5.3.0",
17
+ "commander": "^11.1.0",
18
+ "inquirer": "^13.3.2",
19
+ "pg": "^8.11.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.10.0",
23
+ "@types/pg": "^8.10.0",
24
+ "tsx": "^4.7.0",
25
+ "typescript": "^5.3.0"
26
+ }
27
+ }
@@ -0,0 +1,259 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { PostgresDriver } from '../drivers/postgres.js';
4
+ import { promises as fs } from 'fs';
5
+ import path from 'path';
6
+ import os from 'os';
7
+
8
+ interface ConnectionInfo {
9
+ url: string;
10
+ database: string;
11
+ host: string;
12
+ port: number;
13
+ username: string;
14
+ password: string;
15
+ lastConnected: string;
16
+ enabled: boolean;
17
+ }
18
+
19
+ const CONFIG_DIR = path.join(os.homedir(), '.dbcli');
20
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'connections.json');
21
+
22
+ let connections: Map<string, ConnectionInfo> = new Map();
23
+
24
+ async function loadConnections(): Promise<void> {
25
+ try {
26
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
27
+ const data = await fs.readFile(CONFIG_FILE, 'utf-8');
28
+ const parsed = JSON.parse(data) as Record<string, ConnectionInfo>;
29
+ connections = new Map(Object.entries(parsed));
30
+ } catch {
31
+ connections = new Map();
32
+ }
33
+ }
34
+
35
+ async function saveConnections(): Promise<void> {
36
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
37
+ const obj = Object.fromEntries(connections);
38
+ await fs.writeFile(CONFIG_FILE, JSON.stringify(obj, null, 2), 'utf-8');
39
+ }
40
+
41
+ export function getConnections(): Map<string, ConnectionInfo> {
42
+ return connections;
43
+ }
44
+
45
+ export async function getActiveConnection(): Promise<ConnectionInfo | null> {
46
+ await loadConnections();
47
+ for (const conn of connections.values()) {
48
+ if (conn.enabled) {
49
+ return conn;
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+
55
+ export async function getConnectionByName(name: string): Promise<ConnectionInfo | null> {
56
+ await loadConnections();
57
+ return connections.get(name) || null;
58
+ }
59
+
60
+ async function promptCredentials(): Promise<{ username: string; password: string }> {
61
+ const answers = await inquirer.prompt([
62
+ {
63
+ type: 'input',
64
+ name: 'username',
65
+ message: 'Username:',
66
+ default: 'postgres',
67
+ },
68
+ {
69
+ type: 'password',
70
+ name: 'password',
71
+ message: 'Password:',
72
+ mask: '*',
73
+ },
74
+ ]);
75
+ return answers;
76
+ }
77
+
78
+ function isUrl(str: string): boolean {
79
+ return str.includes('://');
80
+ }
81
+
82
+ function buildUrl(info: ConnectionInfo, username: string, password: string): string {
83
+ return `postgresql://${username}:${password}@${info.host}:${info.port}/${info.database}`;
84
+ }
85
+
86
+ export async function showConnections(): Promise<void> {
87
+ await loadConnections();
88
+
89
+ if (connections.size === 0) {
90
+ console.log(chalk.yellow('No saved connections.'));
91
+ console.log(chalk.cyan('Use "dbcli connect <url>" to add a new connection.'));
92
+ return;
93
+ }
94
+
95
+ const activeConnection = Array.from(connections.values()).find((conn) => conn.enabled);
96
+ if (activeConnection) {
97
+ console.log(chalk.green(`Current connected database: "${activeConnection.database}" (user: ${activeConnection.username})`));
98
+ } else {
99
+ console.log(chalk.yellow('No database is currently connected.'));
100
+ }
101
+
102
+ console.log(chalk.cyan('Saved connections:'));
103
+ const rows = Array.from(connections.entries()).map(([name, info]) => ({
104
+ username: info.username || '-',
105
+ database: info.database,
106
+ host: info.host,
107
+ port: info.port,
108
+ status: info.enabled ? 'enabled' : 'disabled',
109
+ lastConnected: info.lastConnected,
110
+ }));
111
+ console.table(rows);
112
+ }
113
+
114
+ export async function deleteConnection(name: string): Promise<void> {
115
+ await loadConnections();
116
+
117
+ if (!connections.has(name)) {
118
+ console.error(chalk.red(`Connection "${name}" not found.`));
119
+ process.exit(1);
120
+ }
121
+
122
+ connections.delete(name);
123
+ await saveConnections();
124
+ console.log(chalk.yellow(`Connection "${name}" deleted.`));
125
+ }
126
+
127
+ export async function connect(nameUrl: string, options?: { username?: string; password?: string; current?: boolean }): Promise<void> {
128
+ await loadConnections();
129
+
130
+ let database: string;
131
+ let host: string;
132
+ let port: number;
133
+ let existing: ConnectionInfo | null = null;
134
+ let urlUsername = '';
135
+ let urlPassword = '';
136
+
137
+ if (isUrl(nameUrl)) {
138
+ const url = new URL(nameUrl);
139
+ database = url.pathname.slice(1) || 'postgres';
140
+ host = url.hostname;
141
+ port = parseInt(url.port) || 5432;
142
+ urlUsername = url.username;
143
+ urlPassword = url.password;
144
+ existing = connections.get(database) || null;
145
+ } else {
146
+ database = nameUrl;
147
+ existing = connections.get(database) || null;
148
+ if (!existing) {
149
+ console.error(chalk.red(`Connection "${database}" not found.`));
150
+ console.log(chalk.cyan('Use "dbcli connect <url>" to add a new connection.'));
151
+ process.exit(1);
152
+ }
153
+ host = existing.host;
154
+ port = existing.port;
155
+ }
156
+
157
+ let credentials: { username: string; password: string };
158
+
159
+ if (options?.current) {
160
+ const activeConn = Array.from(connections.values()).find((conn) => conn.enabled);
161
+ if (!activeConn) {
162
+ console.error(chalk.red('No currently connected database found.'));
163
+ process.exit(1);
164
+ }
165
+ credentials = { username: activeConn.username, password: activeConn.password };
166
+ } else if (options?.username && options?.password) {
167
+ credentials = { username: options.username, password: options.password };
168
+ } else if (urlUsername || urlPassword) {
169
+ credentials = { username: urlUsername, password: urlPassword };
170
+ } else if (existing && existing.username && existing.password) {
171
+ const useExisting = await inquirer.prompt([
172
+ {
173
+ type: 'input',
174
+ name: 'value',
175
+ message: `You have an existing account "${existing.username}". Enter "y" to use it or anything else to re-login:`,
176
+ default: 'y',
177
+ },
178
+ ]);
179
+
180
+ if (useExisting.value.toLowerCase() === 'y') {
181
+ credentials = { username: existing.username, password: existing.password };
182
+ } else {
183
+ credentials = await promptCredentials();
184
+ }
185
+ } else {
186
+ credentials = await promptCredentials();
187
+ }
188
+
189
+ let finalUrl: string;
190
+ if (existing) {
191
+ finalUrl = buildUrl(existing, credentials.username, credentials.password);
192
+ } else {
193
+ finalUrl = `postgresql://${credentials.username}:${credentials.password}@${host}:${port}/${database}`;
194
+ }
195
+
196
+ const driver = new PostgresDriver();
197
+
198
+ try {
199
+ console.log(chalk.cyan(`Connecting to db("${database}")...`));
200
+ await driver.connect(finalUrl);
201
+
202
+ for (const conn of connections.values()) {
203
+ conn.enabled = false;
204
+ }
205
+
206
+ const connectionInfo: ConnectionInfo = {
207
+ url: `postgresql://${host}:${port}/${database}`,
208
+ database,
209
+ host,
210
+ port,
211
+ username: credentials.username,
212
+ password: credentials.password,
213
+ lastConnected: new Date().toISOString(),
214
+ enabled: true,
215
+ };
216
+
217
+ connections.set(database, connectionInfo);
218
+ await saveConnections();
219
+
220
+ console.log(chalk.green(`Connected to db("${database}") as user("${credentials.username}") successfully!`));
221
+ process.exit(0);
222
+ } catch (error) {
223
+ const message = error instanceof Error ? error.message : String(error);
224
+ console.error(chalk.red(`Connection failed: ${message}`));
225
+ process.exit(1);
226
+ }
227
+ }
228
+
229
+ export async function disconnect(name: string): Promise<void> {
230
+ await loadConnections();
231
+
232
+ if (!connections.has(name)) {
233
+ console.error(chalk.red(`Connection "${name}" not found.`));
234
+ process.exit(1);
235
+ }
236
+
237
+ const conn = connections.get(name)!;
238
+ conn.enabled = false;
239
+ await saveConnections();
240
+
241
+ console.log(chalk.yellow(`Disconnected from "${name}".`));
242
+ }
243
+
244
+ export async function logout(name: string): Promise<void> {
245
+ await loadConnections();
246
+
247
+ if (!connections.has(name)) {
248
+ console.error(chalk.red(`Connection "${name}" not found.`));
249
+ process.exit(1);
250
+ }
251
+
252
+ const conn = connections.get(name)!;
253
+ conn.enabled = false;
254
+ conn.username = '';
255
+ conn.password = '';
256
+ await saveConnections();
257
+
258
+ console.log(chalk.yellow(`Logged out from "${name}". Username and password cleared.`));
259
+ }
@@ -0,0 +1,51 @@
1
+ import chalk from 'chalk';
2
+ import { PostgresDriver } from '../drivers/postgres.js';
3
+ import { getActiveConnection } from './connect.js';
4
+
5
+ async function getActiveDriver(): Promise<{ driver: PostgresDriver; connName: string } | null> {
6
+ const activeConn = await getActiveConnection();
7
+ if (!activeConn) {
8
+ return null;
9
+ }
10
+
11
+ const url = `postgresql://${activeConn.username}:${activeConn.password}@${activeConn.host}:${activeConn.port}/${activeConn.database}`;
12
+ const driver = new PostgresDriver();
13
+ await driver.connect(url);
14
+
15
+ const connName = activeConn.database;
16
+ return { driver, connName };
17
+ }
18
+
19
+ export async function executeQuery(sql: string): Promise<void> {
20
+ const result = await getActiveDriver();
21
+
22
+ if (!result) {
23
+ console.log(chalk.red('No active database connection.'));
24
+ console.log(chalk.cyan('Please use "dbcli connect <url>" to connect to a database first.'));
25
+ process.exit(1);
26
+ }
27
+
28
+ const { driver, connName } = result;
29
+
30
+ try {
31
+ console.log(chalk.gray(`Executing on database "${connName}":`));
32
+ console.log(chalk.cyan(sql));
33
+ console.log();
34
+
35
+ const queryResult = await driver.query(sql);
36
+
37
+ if (queryResult.rows.length === 0) {
38
+ console.log(chalk.yellow(`Query executed successfully. No rows returned.`));
39
+ console.log(chalk.gray(`Row count: ${queryResult.rowCount}`));
40
+ } else {
41
+ console.table(queryResult.rows);
42
+ console.log(chalk.gray(`Total: ${queryResult.rows.length} row(s)`));
43
+ }
44
+ } catch (error) {
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ console.error(chalk.red(`Query failed: ${message}`));
47
+ process.exit(1);
48
+ } finally {
49
+ await driver.disconnect();
50
+ }
51
+ }
@@ -0,0 +1,77 @@
1
+ import chalk from 'chalk';
2
+ import { PostgresDriver } from '../drivers/postgres.js';
3
+ import { getActiveConnection } from './connect.js';
4
+
5
+ async function getActiveDriver(): Promise<{ driver: PostgresDriver; connName: string } | null> {
6
+ const activeConn = await getActiveConnection();
7
+ if (!activeConn) {
8
+ return null;
9
+ }
10
+
11
+ const url = `postgresql://${activeConn.username}:${activeConn.password}@${activeConn.host}:${activeConn.port}/${activeConn.database}`;
12
+ const driver = new PostgresDriver();
13
+ await driver.connect(url);
14
+
15
+ const connName = activeConn.database;
16
+ return { driver, connName };
17
+ }
18
+
19
+ export async function listTables(): Promise<void> {
20
+ const result = await getActiveDriver();
21
+
22
+ if (!result) {
23
+ console.log(chalk.red('No active database connection.'));
24
+ console.log(chalk.cyan('Please use "dbcli connect <url>" to connect to a database first.'));
25
+ process.exit(1);
26
+ }
27
+
28
+ const { driver, connName } = result;
29
+
30
+ try {
31
+ const tables = await driver.listTables();
32
+
33
+ if (tables.length === 0) {
34
+ console.log(chalk.yellow(`No tables found in database "${connName}".`));
35
+ } else {
36
+ console.log(chalk.cyan(`Tables in database "${connName}":`));
37
+ console.table(tables);
38
+ }
39
+ } finally {
40
+ await driver.disconnect();
41
+ }
42
+ }
43
+
44
+ export async function describeTable(tableName: string): Promise<void> {
45
+ const result = await getActiveDriver();
46
+
47
+ if (!result) {
48
+ console.log(chalk.red('No active database connection.'));
49
+ console.log(chalk.cyan('Please use "dbcli connect <url>" to connect to a database first.'));
50
+ process.exit(1);
51
+ }
52
+
53
+ const { driver, connName } = result;
54
+
55
+ try {
56
+ const columns = await driver.describeTable('public', tableName);
57
+
58
+ if (columns.length === 0) {
59
+ console.log(chalk.yellow(`Table "${tableName}" not found in database "${connName}".`));
60
+ } else {
61
+ console.log(chalk.cyan(`Structure of table "${tableName}" in database "${connName}":`));
62
+ console.table(columns);
63
+
64
+ const hasDescriptions = columns.some(col => col.description);
65
+ if (hasDescriptions) {
66
+ console.log(chalk.gray('\nColumn descriptions:'));
67
+ columns.forEach(col => {
68
+ if (col.description) {
69
+ console.log(` ${chalk.white(col.name)}: ${chalk.gray(col.description)}`);
70
+ }
71
+ });
72
+ }
73
+ }
74
+ } finally {
75
+ await driver.disconnect();
76
+ }
77
+ }
@@ -0,0 +1,42 @@
1
+ /*
2
+ * @Author: Mingxuan songmingxuan936@gmail.com
3
+ * @Date: 2026-04-04 13:28:20
4
+ * @LastEditors: Mingxuan songmingxuan936@gmail.com
5
+ * @LastEditTime: 2026-04-04 13:28:41
6
+ * @FilePath: /dbcli/src/drivers/interface.ts
7
+ * @Description:
8
+ *
9
+ * Copyright (c) 2026 by ${git_name_email}, All Rights Reserved.
10
+ */
11
+ export interface QueryResult {
12
+ rows: Record<string, unknown>[];
13
+ rowCount: number;
14
+ fields: FieldInfo[];
15
+ }
16
+
17
+ export interface FieldInfo {
18
+ name: string;
19
+ dataTypeID: number;
20
+ }
21
+
22
+ export interface TableInfo {
23
+ schema: string;
24
+ name: string;
25
+ }
26
+
27
+ export interface ColumnInfo {
28
+ name: string;
29
+ dataType: string;
30
+ isNullable: boolean;
31
+ defaultValue: string | null;
32
+ description: string | null;
33
+ }
34
+
35
+ export interface DatabaseDriver {
36
+ connect(url: string): Promise<void>;
37
+ disconnect(): Promise<void>;
38
+ isConnected(): boolean;
39
+ query(sql: string): Promise<QueryResult>;
40
+ listTables(): Promise<TableInfo[]>;
41
+ describeTable(schema: string, table: string): Promise<ColumnInfo[]>;
42
+ }
@@ -0,0 +1,97 @@
1
+ /*
2
+ * @Author: Mingxuan songmingxuan936@gmail.com
3
+ * @Date: 2026-04-05 17:01:59
4
+ * @LastEditors: Mingxuan songmingxuan936@gmail.com
5
+ * @LastEditTime: 2026-04-05 17:42:59
6
+ * @FilePath: /dbcli/src/drivers/postgres.ts
7
+ * @Description:
8
+ *
9
+ * Copyright (c) 2026 by ${git_name_email}, All Rights Reserved.
10
+ */
11
+ import pg from 'pg';
12
+ import type { DatabaseDriver, QueryResult, TableInfo, ColumnInfo } from './interface.js';
13
+
14
+ export class PostgresDriver implements DatabaseDriver {
15
+ private client: pg.PoolClient | null = null;
16
+ private pool: pg.Pool | null = null;
17
+ private connected = false;
18
+
19
+ async connect(url: string): Promise<void> {
20
+ const connectionUrl = new URL(url);
21
+
22
+ this.pool = new pg.Pool({
23
+ host: connectionUrl.hostname,
24
+ port: parseInt(connectionUrl.port) || 5432,
25
+ user: connectionUrl.username,
26
+ password: connectionUrl.password,
27
+ database: connectionUrl.pathname.slice(1) || 'postgres',
28
+ ssl: connectionUrl.searchParams.get('sslmode') === 'require' ? { rejectUnauthorized: false } : undefined,
29
+ });
30
+
31
+ this.client = await this.pool.connect();
32
+ this.connected = true;
33
+ }
34
+
35
+ async disconnect(): Promise<void> {
36
+ if (this.client) {
37
+ this.client.release();
38
+ this.client = null;
39
+ }
40
+ if (this.pool) {
41
+ await this.pool.end();
42
+ this.pool = null;
43
+ }
44
+ this.connected = false;
45
+ }
46
+
47
+ isConnected(): boolean {
48
+ return this.connected && this.client !== null;
49
+ }
50
+
51
+ async query(sql: string, params?: unknown[]): Promise<QueryResult> {
52
+ if (!this.client) {
53
+ throw new Error('Not connected to database');
54
+ }
55
+
56
+ const result = await this.client.query(sql, params);
57
+ return {
58
+ rows: result.rows,
59
+ rowCount: result.rowCount ?? 0,
60
+ fields: result.fields.map((f: pg.FieldDef) => ({
61
+ name: f.name,
62
+ dataTypeID: f.dataTypeID,
63
+ })),
64
+ };
65
+ }
66
+
67
+ async listTables(): Promise<TableInfo[]> {
68
+ const result = await this.query(`
69
+ SELECT schemaname as schema, tablename as name
70
+ FROM pg_tables
71
+ WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
72
+ ORDER BY schemaname, tablename
73
+ `);
74
+ return result.rows as unknown as TableInfo[];
75
+ }
76
+
77
+ async describeTable(schema: string, table: string): Promise<ColumnInfo[]> {
78
+ const result = await this.query(`
79
+ SELECT
80
+ a.attname as name,
81
+ format_type(a.atttypid, a.atttypmod) as data_type,
82
+ CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END as is_nullable,
83
+ pg_get_expr(ad.adbin, a.attrelid) as default_value,
84
+ col_description(a.attrelid, a.attnum) as description
85
+ FROM pg_attribute a
86
+ JOIN pg_class c ON c.oid = a.attrelid
87
+ JOIN pg_namespace n ON n.oid = c.relnamespace
88
+ LEFT JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
89
+ WHERE n.nspname = $1
90
+ AND c.relname = $2
91
+ AND a.attnum > 0
92
+ AND NOT a.attisdropped
93
+ ORDER BY a.attnum
94
+ `, [schema, table]);
95
+ return result.rows as unknown as ColumnInfo[];
96
+ }
97
+ }
package/src/index.ts ADDED
@@ -0,0 +1,85 @@
1
+ import { Command } from 'commander';
2
+ import { connect, disconnect, logout, showConnections as statusConnections, deleteConnection } from './commands/connect.js';
3
+ import { listTables, describeTable } from './commands/tables.js';
4
+ import { executeQuery } from './commands/query.js';
5
+
6
+ const program = new Command();
7
+
8
+ program
9
+ .name('dbcli')
10
+ .description('Database CLI tool')
11
+ .version('0.1.0')
12
+ .addHelpCommand(false);
13
+
14
+ program
15
+ .command('connect')
16
+ .description('Connect to a database (-u <user> -p <pass> for credentials, -c to use current)')
17
+ .argument('<db-name|url>', 'Connection name (for existing) or full URL (for new connection)')
18
+ .option('-u, --username <username>', 'Username for authentication')
19
+ .option('-p, --password <password>', 'Password for authentication')
20
+ .option('-c, --current', 'Use credentials from currently connected database')
21
+ .action(async (nameUrl: string, options) => {
22
+ await connect(nameUrl, options);
23
+ });
24
+
25
+ program
26
+ .option('-s, --status', 'Show all saved connections status')
27
+ .action(async (options, cmd) => {
28
+ if (cmd.args && cmd.args.length > 0) {
29
+ console.error(`error: unknown command '${cmd.args[0]}'`);
30
+ process.exit(1);
31
+ }
32
+ if (options.status) {
33
+ await statusConnections();
34
+ process.exit(0);
35
+ }
36
+ });
37
+
38
+ program
39
+ .command('disconnect')
40
+ .description('Disconnect from a database (keeps credentials)')
41
+ .argument('<db-name>', 'Connection name to disconnect')
42
+ .action(async (name: string) => {
43
+ await disconnect(name);
44
+ });
45
+
46
+ program
47
+ .command('logout')
48
+ .description('Logout from a connection (clears credentials)')
49
+ .argument('<db-name>', 'Connection name to logout')
50
+ .action(async (name: string) => {
51
+ await logout(name);
52
+ });
53
+
54
+ program
55
+ .command('delete')
56
+ .description('Delete a saved connection completely')
57
+ .argument('<db-name>', 'Connection name to delete')
58
+ .action(async (name: string) => {
59
+ await deleteConnection(name);
60
+ });
61
+
62
+ program
63
+ .command('tables')
64
+ .description('List all tables in the connected database')
65
+ .action(async () => {
66
+ await listTables();
67
+ });
68
+
69
+ program
70
+ .command('describe')
71
+ .description('Show the structure of a table')
72
+ .argument('<table>', 'Table name to describe')
73
+ .action(async (table: string) => {
74
+ await describeTable(table);
75
+ });
76
+
77
+ program
78
+ .command('query')
79
+ .description('Execute a SQL query')
80
+ .argument('<sql>', 'SQL query to execute (use quotes for complex queries)')
81
+ .action(async (sql: string) => {
82
+ await executeQuery(sql);
83
+ });
84
+
85
+ program.parse();
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }