@dbstudio/cli 0.1.7

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.
@@ -0,0 +1,418 @@
1
+ import fs from "node:fs";
2
+ import type { DatabaseConfig, DatabaseType } from "@dbstudio/types";
3
+ import chalk from "chalk";
4
+ import { Command } from "commander";
5
+ import ora from "ora";
6
+ import prompts from "prompts";
7
+ import { Agent } from "../agents/index";
8
+
9
+ export const connectCommand = new Command("connect")
10
+ .description("Connect to a database and bridge to DBStudio cloud")
11
+ .requiredOption("-t, --token <token>", "DBStudio connection token")
12
+ .option("--type <type>", "Database type: postgresql, mysql, sqlite, libsql")
13
+ .option("-h, --host <host>", "Database host", "localhost")
14
+ .option("-p, --port <port>", "Database port")
15
+ .option("-d, --database <database>", "Database name")
16
+ .option("-u, --user <user>", "Database username")
17
+ .option("--password <password>", "Database password")
18
+ .option("-a, --auth-token <token>", "Turso/libSQL auth token")
19
+ .option("-f, --file <filepath>", "SQLite database file path")
20
+ .option(
21
+ "--url <url>",
22
+ "Database connection URL (replaces host/port/db/user/pass)",
23
+ )
24
+ .option("--ssh-host <host>", "SSH tunnel host")
25
+ .option("--ssh-port <port>", "SSH tunnel port", "22")
26
+ .option("--ssh-user <user>", "SSH tunnel username")
27
+ .option("--ssh-password <password>", "SSH tunnel password")
28
+ .option("--ssh-key <path>", "SSH tunnel private key path")
29
+ .option("--ssh-passphrase <passphrase>", "SSH tunnel private key passphrase")
30
+ .option("--ssl", "Enable SSL connection", false)
31
+ .option("--workspace <id>", "Legacy workspace ID (deprecated)", "")
32
+ .option(
33
+ "--server <url>",
34
+ "DBStudio server URL",
35
+ process.env.DBSTUDIO_SERVER_URL || "wss://api.dbstudio.tech/ws/agent",
36
+ )
37
+ .action(async (options) => {
38
+ let config: DatabaseConfig = {
39
+ type: options.type as DatabaseType,
40
+ url: options.url,
41
+ host: options.host,
42
+ port: options.port ? Number.parseInt(options.port, 10) : undefined,
43
+ database: options.database,
44
+ username: options.user,
45
+ password: options.password,
46
+ filepath: options.file,
47
+ ssl: options.ssl,
48
+ authToken: options.authToken,
49
+ };
50
+
51
+ // --- Smart URL Detection ---
52
+ // If --database contains a URL-like string, move it to config.url
53
+ if (options.database?.includes("://") && !config.url) {
54
+ config.url = options.database;
55
+ config.database = undefined;
56
+ }
57
+
58
+ if (options.sshHost) {
59
+ config.ssh = {
60
+ host: options.sshHost,
61
+ port: options.sshPort ? Number.parseInt(options.sshPort, 10) : 22,
62
+ username: options.sshUser,
63
+ password: options.sshPassword,
64
+ privateKey: options.sshKey
65
+ ? fs.readFileSync(options.sshKey, "utf8")
66
+ : undefined,
67
+ passphrase: options.sshPassphrase,
68
+ };
69
+ }
70
+
71
+ const spinner = ora("Initializing connection...");
72
+ if (
73
+ !config.type &&
74
+ !config.database &&
75
+ !config.filepath &&
76
+ !config.url &&
77
+ !config.authToken
78
+ ) {
79
+ console.log(chalk.cyan("Welcome to DBStudio CLI Setup!"));
80
+ console.log(chalk.gray("Let's connect your database.\n"));
81
+
82
+ const response = await prompts(
83
+ [
84
+ {
85
+ type: "select",
86
+ name: "type",
87
+ message: "Database Type",
88
+ choices: [
89
+ { title: "PostgreSQL", value: "postgresql" },
90
+ { title: "MySQL", value: "mysql" },
91
+ { title: "SQLite", value: "sqlite" },
92
+ { title: "Turso (libSQL)", value: "libsql" },
93
+ ],
94
+ },
95
+ {
96
+ type: (prev) => {
97
+ if (prev === "sqlite") return "text";
98
+ if (prev === "libsql") return null;
99
+ return "select";
100
+ },
101
+ name: "methodOrPath",
102
+ message: (_prev, values) =>
103
+ values.type === "sqlite"
104
+ ? "Database File Path"
105
+ : "Connection Method",
106
+ choices: (_prev, values) =>
107
+ values.type === "sqlite"
108
+ ? []
109
+ : [
110
+ { title: "Host / Port / Database", value: "params" },
111
+ { title: "Connection URL", value: "url" },
112
+ ],
113
+ initial: (_prev, values) =>
114
+ values.type === "sqlite" ? "./local.db" : 0,
115
+ },
116
+ {
117
+ type: (_prev, values) =>
118
+ values.type === "libsql" || values.methodOrPath === "url"
119
+ ? "text"
120
+ : null,
121
+ name: "url",
122
+ message: (_prev, values) =>
123
+ values.type === "libsql" ? "libSQL/Turso URL" : "Connection URL",
124
+ initial: (_prev, values) =>
125
+ values.type === "libsql"
126
+ ? "libsql://[your-db].turso.io"
127
+ : values.type === "postgresql"
128
+ ? "postgresql://user:pass@localhost:5432/db"
129
+ : "mysql://user:pass@localhost:3306/db",
130
+ },
131
+ {
132
+ type: (_prev, values) => (values.type === "libsql" ? "text" : null),
133
+ name: "authToken",
134
+ message: "Auth Token (optional for local libsql)",
135
+ },
136
+ {
137
+ type: (_prev, values) =>
138
+ values.type !== "sqlite" &&
139
+ values.type !== "libsql" &&
140
+ values.methodOrPath === "params"
141
+ ? "text"
142
+ : null,
143
+ name: "host",
144
+ message: "Host",
145
+ initial: "localhost",
146
+ },
147
+ {
148
+ type: (_prev, values) =>
149
+ values.type !== "sqlite" &&
150
+ values.type !== "libsql" &&
151
+ values.methodOrPath === "params"
152
+ ? "number"
153
+ : null,
154
+ name: "port",
155
+ message: "Port",
156
+ initial: (_prev, values) =>
157
+ values.type === "postgresql" ? 5432 : 3306,
158
+ },
159
+ {
160
+ type: (_prev, values) =>
161
+ values.type !== "sqlite" &&
162
+ values.type !== "libsql" &&
163
+ values.methodOrPath === "params"
164
+ ? "text"
165
+ : null,
166
+ name: "database",
167
+ message: "Database Name",
168
+ },
169
+ {
170
+ type: (_prev, values) =>
171
+ values.type !== "sqlite" &&
172
+ values.type !== "libsql" &&
173
+ values.methodOrPath === "params"
174
+ ? "text"
175
+ : null,
176
+ name: "username",
177
+ message: "Username",
178
+ },
179
+ {
180
+ type: (_prev, values) =>
181
+ values.type !== "sqlite" &&
182
+ values.type !== "libsql" &&
183
+ values.methodOrPath === "params"
184
+ ? "password"
185
+ : null,
186
+ name: "password",
187
+ message: "Password",
188
+ },
189
+ {
190
+ type: (_prev, values) =>
191
+ values.type !== "sqlite" && values.type !== "libsql"
192
+ ? "confirm"
193
+ : null,
194
+ name: "useSsh",
195
+ message: "Use SSH Tunnel?",
196
+ initial: false,
197
+ },
198
+ {
199
+ type: (_prev, values) => (values.useSsh ? "text" : null),
200
+ name: "sshHost",
201
+ message: "SSH Host",
202
+ },
203
+ {
204
+ type: (_prev, values) => (values.useSsh ? "number" : null),
205
+ name: "sshPort",
206
+ message: "SSH Port",
207
+ initial: 22,
208
+ },
209
+ {
210
+ type: (_prev, values) => (values.useSsh ? "text" : null),
211
+ name: "sshUser",
212
+ message: "SSH Username",
213
+ },
214
+ {
215
+ type: (_prev, values) => (values.useSsh ? "select" : null),
216
+ name: "sshAuthType",
217
+ message: "SSH Auth Type",
218
+ choices: [
219
+ { title: "Password", value: "password" },
220
+ { title: "Private Key", value: "key" },
221
+ ],
222
+ },
223
+ {
224
+ type: (_prev, values) =>
225
+ values.sshAuthType === "password" ? "password" : null,
226
+ name: "sshPassword",
227
+ message: "SSH Password",
228
+ },
229
+ {
230
+ type: (_prev, values) =>
231
+ values.sshAuthType === "key" ? "text" : null,
232
+ name: "sshKeyPath",
233
+ message: "SSH Key Path",
234
+ initial: "~/.ssh/id_rsa",
235
+ },
236
+ {
237
+ type: (_prev, values) =>
238
+ values.type !== "sqlite" && values.type !== "libsql"
239
+ ? "confirm"
240
+ : null,
241
+ name: "ssl",
242
+ message: "Enable SSL?",
243
+ initial: false,
244
+ },
245
+ ],
246
+ {
247
+ onCancel: () => {
248
+ console.log(chalk.yellow("\nSetup cancelled."));
249
+ process.exit(0);
250
+ },
251
+ },
252
+ );
253
+
254
+ config = {
255
+ type: response.type,
256
+ url: response.url,
257
+ host: response.host,
258
+ port: response.port,
259
+ database: response.database,
260
+ username: response.username,
261
+ password: response.password,
262
+ filepath: response.methodOrPath, // If type was sqlite, methodOrPath holds the filepath
263
+ authToken: response.authToken,
264
+ ssl: response.ssl,
265
+ };
266
+
267
+ if (response.type === "sqlite") {
268
+ config.filepath = response.methodOrPath;
269
+ }
270
+
271
+ if (response.useSsh) {
272
+ try {
273
+ config.ssh = {
274
+ host: response.sshHost,
275
+ port: response.sshPort,
276
+ username: response.sshUser,
277
+ password: response.sshPassword,
278
+ privateKey: response.sshKeyPath
279
+ ? fs.readFileSync(response.sshKeyPath, "utf8")
280
+ : undefined,
281
+ };
282
+ } catch (err) {
283
+ spinner.fail(
284
+ chalk.red(
285
+ `Failed to read SSH key: ${err instanceof Error ? err.message : String(err)}`,
286
+ ),
287
+ );
288
+ process.exit(1);
289
+ }
290
+ }
291
+ }
292
+
293
+ // --- Password Prompt check ---
294
+ // If flags were used but password is missing for non-sqlite/libsql/url connections, prompt for it
295
+ if (
296
+ config.type !== "sqlite" &&
297
+ config.type !== "libsql" &&
298
+ !config.url &&
299
+ config.host &&
300
+ !config.password
301
+ ) {
302
+ const response = await prompts({
303
+ type: "password",
304
+ name: "password",
305
+ message: `Password for ${config.username || "user"}@${config.host}`,
306
+ });
307
+ if (response.password) {
308
+ config.password = response.password;
309
+ }
310
+ }
311
+
312
+ spinner.start();
313
+
314
+ try {
315
+ // Validation
316
+ if (!["postgresql", "mysql", "sqlite", "libsql"].includes(config.type)) {
317
+ spinner.fail(chalk.red(`Invalid database type: ${config.type}`));
318
+ process.exit(1);
319
+ }
320
+
321
+ // Set default ports if missing and not sqlite/libsql
322
+ if (
323
+ config.type !== "sqlite" &&
324
+ config.type !== "libsql" &&
325
+ !config.port &&
326
+ !config.url
327
+ ) {
328
+ config.port = config.type === "postgresql" ? 5432 : 3306;
329
+ }
330
+
331
+ if (config.type === "sqlite" && !config.filepath) {
332
+ spinner.fail(chalk.red("SQLite requires --file option or input"));
333
+ process.exit(1);
334
+ }
335
+
336
+ if (config.type === "libsql" && !config.url) {
337
+ spinner.fail(chalk.red("libSQL requires a connection URL"));
338
+ process.exit(1);
339
+ }
340
+
341
+ if (
342
+ config.type !== "sqlite" &&
343
+ config.type !== "libsql" &&
344
+ !config.database &&
345
+ !config.url
346
+ ) {
347
+ spinner.fail(chalk.red("Database name or URL is required"));
348
+ process.exit(1);
349
+ }
350
+
351
+ spinner.text = "Connecting to database...";
352
+
353
+ // Create and start agent
354
+ const agent = new Agent({
355
+ serverUrl: options.server,
356
+ token: options.token,
357
+ dbConfig: config as any,
358
+ });
359
+
360
+ await agent.connect();
361
+ spinner.succeed(chalk.green("Connected to DBStudio!"));
362
+
363
+ console.log(
364
+ chalk.gray("\nAgent is running. Press Ctrl+C to disconnect.\n"),
365
+ );
366
+
367
+ // Print equivalent command for next time
368
+ let cmd = `dbstudio connect --token "${options.token}" --type ${config.type}`;
369
+ if (config.url) {
370
+ cmd += ` --url "${config.url}"`;
371
+ }
372
+ if (config.authToken) {
373
+ cmd += ` --auth-token "${config.authToken}"`;
374
+ }
375
+ if (config.type === "sqlite") {
376
+ cmd += ` --file "${config.filepath}"`;
377
+ }
378
+ if (!config.url && config.type !== "sqlite" && config.type !== "libsql") {
379
+ if (config.host) cmd += ` --host "${config.host}"`;
380
+ if (config.port) cmd += ` --port ${config.port}`;
381
+ if (config.database) cmd += ` --database "${config.database}"`;
382
+ if (config.username) cmd += ` --user "${config.username}"`;
383
+ if (config.password) cmd += ` --password "${config.password}"`;
384
+ if (config.ssl) cmd += " --ssl";
385
+ }
386
+
387
+ if (config.ssh) {
388
+ cmd += ` --ssh-host "${config.ssh.host}" --ssh-user "${config.ssh.username}"`;
389
+ if (config.ssh.port !== 22) cmd += ` --ssh-port ${config.ssh.port}`;
390
+ }
391
+ console.log(chalk.gray("To skip prompts next time, run:"));
392
+ console.log(chalk.cyan(`${cmd}\n`));
393
+
394
+ console.log(
395
+ chalk.cyan("Database:"),
396
+ config.database || config.filepath || config.url,
397
+ );
398
+ console.log(chalk.cyan("Type:"), config.type);
399
+ console.log(chalk.cyan("Server:"), options.server);
400
+
401
+ // Handle graceful shutdown
402
+ process.on("SIGINT", async () => {
403
+ console.log(chalk.yellow("\n\nDisconnecting..."));
404
+ await agent.disconnect();
405
+ console.log(chalk.green("Disconnected."));
406
+ process.exit(0);
407
+ });
408
+
409
+ // Keep process alive
410
+ await new Promise(() => {});
411
+ } catch (error) {
412
+ spinner.fail(chalk.red("Connection failed"));
413
+ console.error(
414
+ chalk.red(error instanceof Error ? error.message : "Unknown error"),
415
+ );
416
+ process.exit(1);
417
+ }
418
+ });
@@ -0,0 +1,37 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import chalk from "chalk";
5
+ import { Command } from "commander";
6
+
7
+ const CONFIG_DIR = path.join(os.homedir(), ".dbstudio");
8
+ const STATUS_FILE = path.join(CONFIG_DIR, "status.json");
9
+
10
+ export const disconnectCommand = new Command("disconnect")
11
+ .description("Disconnect the running DBStudio agent")
12
+ .action(() => {
13
+ try {
14
+ if (!fs.existsSync(STATUS_FILE)) {
15
+ console.log(chalk.yellow("No active connection to disconnect."));
16
+ return;
17
+ }
18
+
19
+ const status = JSON.parse(fs.readFileSync(STATUS_FILE, "utf-8"));
20
+
21
+ if (status.pid) {
22
+ try {
23
+ process.kill(status.pid, "SIGINT");
24
+ console.log(chalk.green("Sent disconnect signal to agent."));
25
+ } catch (_error) {
26
+ console.log(chalk.yellow("Agent process not found. Cleaning up..."));
27
+ }
28
+ }
29
+
30
+ // Clean up status file
31
+ fs.unlinkSync(STATUS_FILE);
32
+ console.log(chalk.green("Disconnected successfully."));
33
+ } catch (error) {
34
+ console.log(chalk.red("Error disconnecting"));
35
+ console.error(error instanceof Error ? error.message : "Unknown error");
36
+ }
37
+ });
@@ -0,0 +1,54 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import chalk from "chalk";
5
+ import { Command } from "commander";
6
+
7
+ const CONFIG_DIR = path.join(os.homedir(), ".dbstudio");
8
+ const STATUS_FILE = path.join(CONFIG_DIR, "status.json");
9
+
10
+ export const statusCommand = new Command("status")
11
+ .description("Check the status of the DBStudio agent")
12
+ .action(() => {
13
+ try {
14
+ if (!fs.existsSync(STATUS_FILE)) {
15
+ console.log(chalk.yellow("No active connection."));
16
+ console.log(chalk.gray("Run 'dbstudio connect' to start an agent."));
17
+ return;
18
+ }
19
+
20
+ const status = JSON.parse(fs.readFileSync(STATUS_FILE, "utf-8"));
21
+
22
+ console.log(chalk.cyan("Connection Status"));
23
+ console.log(chalk.gray("─".repeat(40)));
24
+ console.log(chalk.white("Status:"), getStatusBadge(status.status));
25
+ console.log(chalk.white("Database:"), status.database);
26
+ console.log(chalk.white("Type:"), status.type);
27
+ console.log(
28
+ chalk.white("Connected at:"),
29
+ new Date(status.connectedAt).toLocaleString(),
30
+ );
31
+
32
+ if (status.pid) {
33
+ console.log(chalk.white("Process ID:"), status.pid);
34
+ }
35
+ } catch (_error) {
36
+ console.log(chalk.red("Error reading status"));
37
+ console.log(chalk.gray("Run 'dbstudio connect' to start an agent."));
38
+ }
39
+ });
40
+
41
+ function getStatusBadge(status: string): string {
42
+ switch (status) {
43
+ case "connected":
44
+ return chalk.green("● Connected");
45
+ case "connecting":
46
+ return chalk.yellow("◐ Connecting...");
47
+ case "disconnected":
48
+ return chalk.gray("○ Disconnected");
49
+ case "error":
50
+ return chalk.red("✕ Error");
51
+ default:
52
+ return chalk.gray("Unknown");
53
+ }
54
+ }
@@ -0,0 +1,58 @@
1
+ import type { DatabaseConfig, QueryResult, TableInfo } from "@dbstudio/types";
2
+ import { LibSQLDriver } from "./libsql";
3
+ import { MySQLDriver } from "./mysql";
4
+ import { PostgresDriver } from "./postgres";
5
+ import { SQLiteDriver } from "./sqlite";
6
+
7
+ /**
8
+ * Common interface for all database drivers
9
+ */
10
+ export interface DatabaseDriver {
11
+ /** Connect to the database */
12
+ connect(): Promise<void>;
13
+
14
+ /** Disconnect from the database */
15
+ disconnect(): Promise<void>;
16
+
17
+ /** Execute a query and return results */
18
+ query(sql: string, params?: unknown[]): Promise<QueryResult>;
19
+
20
+ /** Get list of tables in the database */
21
+ getTables(schema?: string): Promise<TableInfo[]>;
22
+
23
+ /** Get detailed schema for a specific table */
24
+ /** Get detailed schema for a specific table */
25
+ getTableSchema(table: string, schema?: string): Promise<TableInfo>;
26
+
27
+ /** Insert a row into a table */
28
+ insertRow(table: string, data: Record<string, unknown>): Promise<QueryResult>;
29
+
30
+ /** Update a row in a table */
31
+ updateRow(
32
+ table: string,
33
+ data: Record<string, unknown>,
34
+ where: Record<string, unknown>,
35
+ ): Promise<QueryResult>;
36
+ }
37
+
38
+ /**
39
+ * Factory function to create the appropriate driver based on config
40
+ */
41
+ export function createDriver(config: DatabaseConfig): DatabaseDriver {
42
+ switch (config.type) {
43
+ case "postgresql":
44
+ return new PostgresDriver(config);
45
+ case "mysql":
46
+ return new MySQLDriver(config);
47
+ case "sqlite":
48
+ return new SQLiteDriver(config);
49
+ case "libsql":
50
+ return new LibSQLDriver(config);
51
+ default:
52
+ throw new Error(`Unsupported database type: ${config.type}`);
53
+ }
54
+ }
55
+
56
+ export { MySQLDriver } from "./mysql";
57
+ export { PostgresDriver } from "./postgres";
58
+ export { SQLiteDriver } from "./sqlite";