@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.
package/dist/index.js ADDED
@@ -0,0 +1,1372 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // src/index.ts
4
+ import "dotenv/config";
5
+ import chalk4 from "chalk";
6
+ import { program } from "commander";
7
+
8
+ // src/commands/connect.ts
9
+ import fs3 from "fs";
10
+ import chalk from "chalk";
11
+ import { Command } from "commander";
12
+ import ora from "ora";
13
+ import prompts from "prompts";
14
+
15
+ // src/agents/index.ts
16
+ import fs2 from "fs";
17
+ import { createServer } from "net";
18
+ import os from "os";
19
+ import path from "path";
20
+ import { Client } from "ssh2";
21
+ import WebSocket from "ws";
22
+
23
+ // src/drivers/libsql.ts
24
+ import { createClient } from "@libsql/client";
25
+ var LibSQLDriver = class {
26
+ client = null;
27
+ config;
28
+ constructor(config) {
29
+ this.config = config;
30
+ }
31
+ async connect() {
32
+ if (!this.config.url) {
33
+ throw new Error("libSQL requires a connection URL");
34
+ }
35
+ this.client = createClient({
36
+ url: this.config.url,
37
+ authToken: this.config.authToken
38
+ });
39
+ await this.client.execute("SELECT 1");
40
+ }
41
+ async disconnect() {
42
+ if (this.client) {
43
+ this.client.close();
44
+ this.client = null;
45
+ }
46
+ }
47
+ async query(sql, params) {
48
+ if (!this.client) throw new Error("Not connected");
49
+ const start = Date.now();
50
+ const result = await this.client.execute({
51
+ sql,
52
+ args: params || []
53
+ });
54
+ const executionTimeMs = Date.now() - start;
55
+ return {
56
+ columns: result.columns,
57
+ rows: result.rows.map((row) => {
58
+ const obj = {};
59
+ result.columns.forEach((col, i) => {
60
+ obj[col] = row[i];
61
+ });
62
+ return obj;
63
+ }),
64
+ rowCount: result.rows.length,
65
+ affectedRows: result.rowsAffected,
66
+ executionTimeMs
67
+ };
68
+ }
69
+ async getTables() {
70
+ if (!this.client) throw new Error("Not connected");
71
+ const result = await this.client.execute(
72
+ `SELECT name FROM sqlite_master
73
+ WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
74
+ ORDER BY name`
75
+ );
76
+ const tables = [];
77
+ for (const row of result.rows) {
78
+ const tableName = row[0];
79
+ try {
80
+ const tableInfo = await this.getTableSchema(tableName);
81
+ tables.push(tableInfo);
82
+ } catch (error) {
83
+ console.error(`Failed to get schema for table ${tableName}:`, error);
84
+ }
85
+ }
86
+ return tables;
87
+ }
88
+ async getTableSchema(table) {
89
+ if (!this.client) throw new Error("Not connected");
90
+ const columnsResult = await this.client.execute(
91
+ `PRAGMA table_info("${table}")`
92
+ );
93
+ const columns = columnsResult.rows.map((row) => ({
94
+ name: row[1],
95
+ type: row[2],
96
+ nullable: row[3] === 0,
97
+ defaultValue: row[4] || void 0,
98
+ isPrimaryKey: row[5] === 1,
99
+ isForeignKey: false
100
+ // Will be updated by relations
101
+ }));
102
+ const countResult = await this.client.execute(
103
+ `SELECT COUNT(*) as count FROM "${table}"`
104
+ );
105
+ const rowCount = countResult.rows[0][0];
106
+ const indexListResult = await this.client.execute(
107
+ `PRAGMA index_list("${table}")`
108
+ );
109
+ const indexes = await Promise.all(
110
+ indexListResult.rows.map(async (row) => {
111
+ const indexName = row[1];
112
+ const isUnique = row[2] === 1;
113
+ const indexInfoResult = await this.client.execute(
114
+ `PRAGMA index_info("${indexName}")`
115
+ );
116
+ return {
117
+ name: indexName,
118
+ unique: isUnique,
119
+ columns: indexInfoResult.rows.map((r) => r[2])
120
+ };
121
+ })
122
+ );
123
+ const fkListResult = await this.client.execute(
124
+ `PRAGMA foreign_key_list("${table}")`
125
+ );
126
+ const relations = fkListResult.rows.map((row) => ({
127
+ name: `${table}_${row[3]}_fk`,
128
+ type: "belongsTo",
129
+ fromTable: table,
130
+ fromColumn: row[3],
131
+ toTable: row[2],
132
+ toColumn: row[4]
133
+ }));
134
+ for (const rel of relations) {
135
+ const col = columns.find((c) => c.name === rel.fromColumn);
136
+ if (col) col.isForeignKey = true;
137
+ }
138
+ return {
139
+ name: table,
140
+ schema: "main",
141
+ columns,
142
+ rowCount,
143
+ indexes,
144
+ relations
145
+ };
146
+ }
147
+ async insertRow(table, data) {
148
+ if (!this.client) throw new Error("Not connected");
149
+ const columns = Object.keys(data);
150
+ const values = Object.values(data);
151
+ const placeholders = values.map(() => "?").join(", ");
152
+ const sql = `INSERT INTO "${table}" (${columns.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`;
153
+ return this.query(sql, values);
154
+ }
155
+ async updateRow(table, data, where) {
156
+ if (!this.client) throw new Error("Not connected");
157
+ const setClauses = Object.keys(data).map((c) => `"${c}" = ?`).join(", ");
158
+ const whereClauses = Object.keys(where).map((c) => `"${c}" = ?`).join(" AND ");
159
+ const values = [...Object.values(data), ...Object.values(where)];
160
+ const sql = `UPDATE "${table}" SET ${setClauses} WHERE ${whereClauses}`;
161
+ return this.query(sql, values);
162
+ }
163
+ };
164
+
165
+ // src/drivers/mysql.ts
166
+ import mysql from "mysql2/promise";
167
+ var MySQLDriver = class {
168
+ connection = null;
169
+ config;
170
+ constructor(config) {
171
+ this.config = config;
172
+ }
173
+ async connect() {
174
+ if (this.config.url) {
175
+ this.connection = await mysql.createConnection(this.config.url);
176
+ } else {
177
+ this.connection = await mysql.createConnection({
178
+ host: this.config.host,
179
+ port: this.config.port,
180
+ database: this.config.database,
181
+ user: this.config.username,
182
+ password: this.config.password,
183
+ ssl: this.config.ssl ? {} : void 0
184
+ });
185
+ }
186
+ }
187
+ async disconnect() {
188
+ if (this.connection) {
189
+ await this.connection.end();
190
+ this.connection = null;
191
+ }
192
+ }
193
+ async query(sql, params) {
194
+ if (!this.connection) throw new Error("Not connected");
195
+ const start = Date.now();
196
+ const [rows, fields] = await this.connection.execute(sql, params);
197
+ const executionTimeMs = Date.now() - start;
198
+ const isResultSet = Array.isArray(rows);
199
+ return {
200
+ columns: fields ? fields.map((f) => f.name) : [],
201
+ rows: isResultSet ? rows : [],
202
+ rowCount: isResultSet ? rows.length : 0,
203
+ affectedRows: !isResultSet ? rows.affectedRows : void 0,
204
+ executionTimeMs
205
+ };
206
+ }
207
+ async getTables(schema) {
208
+ if (!this.connection) throw new Error("Not connected");
209
+ const db = schema || this.config.database;
210
+ const [rows] = await this.connection.execute(
211
+ `SELECT table_name
212
+ FROM information_schema.tables
213
+ WHERE table_schema = ? AND table_type = 'BASE TABLE'
214
+ ORDER BY table_name`,
215
+ [db]
216
+ );
217
+ const tables = [];
218
+ for (const row of rows) {
219
+ const tableInfo = await this.getTableSchema(
220
+ row.TABLE_NAME || row.table_name,
221
+ db
222
+ );
223
+ tables.push(tableInfo);
224
+ }
225
+ return tables;
226
+ }
227
+ async getTableSchema(table, schema) {
228
+ if (!this.connection) throw new Error("Not connected");
229
+ const db = schema || this.config.database;
230
+ const [columnsRows] = await this.connection.execute(
231
+ `SELECT
232
+ COLUMN_NAME as column_name,
233
+ DATA_TYPE as data_type,
234
+ IS_NULLABLE as is_nullable,
235
+ COLUMN_DEFAULT as column_default,
236
+ COLUMN_KEY as column_key
237
+ FROM information_schema.columns
238
+ WHERE table_schema = ? AND table_name = ?
239
+ ORDER BY ordinal_position`,
240
+ [db, table]
241
+ );
242
+ const columns = columnsRows.map((row) => ({
243
+ name: row.column_name,
244
+ type: row.data_type,
245
+ nullable: row.is_nullable === "YES",
246
+ defaultValue: row.column_default,
247
+ isPrimaryKey: row.column_key === "PRI",
248
+ isForeignKey: row.column_key === "MUL"
249
+ }));
250
+ const [countRows] = await this.connection.execute(
251
+ `SELECT COUNT(*) as count FROM \`${db}\`.\`${table}\``
252
+ );
253
+ const [indexesRows] = await this.connection.execute(
254
+ `SHOW INDEX FROM \`${table}\` FROM \`${db}\``
255
+ );
256
+ const indexesMap = /* @__PURE__ */ new Map();
257
+ indexesRows.forEach((row) => {
258
+ if (!indexesMap.has(row.Key_name)) {
259
+ indexesMap.set(row.Key_name, {
260
+ name: row.Key_name,
261
+ columns: [],
262
+ unique: row.Non_unique === 0
263
+ });
264
+ }
265
+ indexesMap.get(row.Key_name).columns.push(row.Column_name);
266
+ });
267
+ const indexes = Array.from(indexesMap.values());
268
+ const [relationsRows] = await this.connection.execute(
269
+ `SELECT
270
+ CONSTRAINT_NAME as constraint_name,
271
+ COLUMN_NAME as column_name,
272
+ REFERENCED_TABLE_NAME as referenced_table_name,
273
+ REFERENCED_COLUMN_NAME as referenced_column_name
274
+ FROM information_schema.KEY_COLUMN_USAGE
275
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND REFERENCED_TABLE_NAME IS NOT NULL`,
276
+ [db, table]
277
+ );
278
+ const relations = relationsRows.map((row) => ({
279
+ name: row.constraint_name,
280
+ type: "belongsTo",
281
+ fromTable: table,
282
+ fromColumn: row.column_name,
283
+ toTable: row.referenced_table_name,
284
+ toColumn: row.referenced_column_name
285
+ }));
286
+ return {
287
+ name: table,
288
+ schema: db,
289
+ columns,
290
+ rowCount: Number.parseInt(countRows[0].count, 10),
291
+ indexes,
292
+ relations
293
+ };
294
+ }
295
+ async insertRow(table, data) {
296
+ if (!this.connection) throw new Error("Not connected");
297
+ const columns = Object.keys(data);
298
+ const values = Object.values(data);
299
+ const placeholders = values.map(() => "?").join(", ");
300
+ const sql = `INSERT INTO \`${table}\` (${columns.map((c) => `\`${c}\``).join(", ")}) VALUES (${placeholders})`;
301
+ return this.query(sql, values);
302
+ }
303
+ async updateRow(table, data, where) {
304
+ if (!this.connection) throw new Error("Not connected");
305
+ const setClauses = Object.keys(data).map((c) => `\`${c}\` = ?`).join(", ");
306
+ const whereClauses = Object.keys(where).map((c) => `\`${c}\` = ?`).join(" AND ");
307
+ const values = [...Object.values(data), ...Object.values(where)];
308
+ const sql = `UPDATE \`${table}\` SET ${setClauses} WHERE ${whereClauses}`;
309
+ return this.query(sql, values);
310
+ }
311
+ };
312
+
313
+ // src/drivers/postgres.ts
314
+ import pg from "pg";
315
+ var { Pool } = pg;
316
+ var PostgresDriver = class {
317
+ pool = null;
318
+ config;
319
+ constructor(config) {
320
+ this.config = config;
321
+ }
322
+ async connect() {
323
+ if (this.config.url) {
324
+ this.pool = new Pool({
325
+ connectionString: this.config.url,
326
+ ssl: this.config.ssl ? { rejectUnauthorized: false } : void 0
327
+ });
328
+ } else {
329
+ this.pool = new Pool({
330
+ host: this.config.host,
331
+ port: this.config.port,
332
+ database: this.config.database,
333
+ user: this.config.username,
334
+ password: this.config.password || "",
335
+ ssl: this.config.ssl ? { rejectUnauthorized: false } : void 0
336
+ });
337
+ }
338
+ const client = await this.pool.connect();
339
+ client.release();
340
+ }
341
+ async disconnect() {
342
+ const pool = this.pool;
343
+ this.pool = null;
344
+ if (pool) {
345
+ await pool.end();
346
+ }
347
+ }
348
+ async query(sql, params) {
349
+ if (!this.pool) throw new Error("Not connected");
350
+ const start = Date.now();
351
+ const result = await this.pool.query(sql, params);
352
+ const executionTimeMs = Date.now() - start;
353
+ return {
354
+ columns: result.fields.map((f) => f.name),
355
+ rows: result.rows,
356
+ rowCount: result.rows.length,
357
+ affectedRows: result.rowCount ?? void 0,
358
+ executionTimeMs
359
+ };
360
+ }
361
+ async getTables(schema = "public") {
362
+ if (!this.pool) throw new Error("Not connected");
363
+ const result = await this.pool.query(
364
+ `SELECT table_name
365
+ FROM information_schema.tables
366
+ WHERE table_schema = $1 AND table_type = 'BASE TABLE'
367
+ ORDER BY table_name`,
368
+ [schema]
369
+ );
370
+ const tables = [];
371
+ for (const row of result.rows) {
372
+ const tableInfo = await this.getTableSchema(row.table_name, schema);
373
+ tables.push(tableInfo);
374
+ }
375
+ return tables;
376
+ }
377
+ async getTableSchema(table, schema = "public") {
378
+ if (!this.pool) throw new Error("Not connected");
379
+ const columnsResult = await this.pool.query(
380
+ `SELECT
381
+ column_name,
382
+ data_type,
383
+ is_nullable,
384
+ column_default
385
+ FROM information_schema.columns
386
+ WHERE table_schema = $1 AND table_name = $2
387
+ ORDER BY ordinal_position`,
388
+ [schema, table]
389
+ );
390
+ const constraintsResult = await this.pool.query(
391
+ `SELECT
392
+ ccu.column_name,
393
+ tc.constraint_type
394
+ FROM information_schema.table_constraints tc
395
+ JOIN information_schema.constraint_column_usage ccu
396
+ ON tc.constraint_name = ccu.constraint_name
397
+ AND tc.table_schema = ccu.table_schema
398
+ WHERE tc.table_schema = $1 AND tc.table_name = $2`,
399
+ [schema, table]
400
+ );
401
+ const columnConstraints = /* @__PURE__ */ new Map();
402
+ for (const row of constraintsResult.rows) {
403
+ if (!columnConstraints.has(row.column_name)) {
404
+ columnConstraints.set(row.column_name, []);
405
+ }
406
+ columnConstraints.get(row.column_name)?.push(row.constraint_type);
407
+ }
408
+ const columns = columnsResult.rows.map((row) => {
409
+ const constraints = columnConstraints.get(row.column_name) || [];
410
+ return {
411
+ name: row.column_name,
412
+ type: row.data_type,
413
+ nullable: row.is_nullable === "YES",
414
+ defaultValue: row.column_default,
415
+ isPrimaryKey: constraints.includes("PRIMARY KEY"),
416
+ isForeignKey: constraints.includes("FOREIGN KEY")
417
+ };
418
+ });
419
+ const indexesResult = await this.pool.query(
420
+ `SELECT indexname, indexdef
421
+ FROM pg_indexes
422
+ WHERE schemaname = $1 AND tablename = $2`,
423
+ [schema, table]
424
+ );
425
+ const indexes = indexesResult.rows.map((row) => ({
426
+ name: row.indexname,
427
+ columns: [],
428
+ // Parsing columns from indexdef is complex, leaving empty for now or need regex
429
+ unique: row.indexdef.includes("UNIQUE")
430
+ }));
431
+ const relationsResult = await this.pool.query(
432
+ `SELECT
433
+ tc.constraint_name,
434
+ tc.constraint_type,
435
+ kcu.column_name,
436
+ ccu.table_name AS foreign_table_name,
437
+ ccu.column_name AS foreign_column_name
438
+ FROM information_schema.table_constraints AS tc
439
+ JOIN information_schema.key_column_usage AS kcu
440
+ ON tc.constraint_name = kcu.constraint_name
441
+ AND tc.table_schema = kcu.table_schema
442
+ JOIN information_schema.constraint_column_usage AS ccu
443
+ ON ccu.constraint_name = tc.constraint_name
444
+ AND ccu.table_schema = tc.table_schema
445
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1 AND tc.table_name = $2`,
446
+ [schema, table]
447
+ );
448
+ const relations = relationsResult.rows.map((row) => ({
449
+ name: row.constraint_name,
450
+ type: "belongsTo",
451
+ fromTable: table,
452
+ fromColumn: row.column_name,
453
+ toTable: row.foreign_table_name,
454
+ toColumn: row.foreign_column_name
455
+ }));
456
+ const countResult = await this.pool.query(
457
+ `SELECT COUNT(*) as count FROM "${schema}"."${table}"`
458
+ );
459
+ return {
460
+ name: table,
461
+ schema,
462
+ columns,
463
+ rowCount: Number.parseInt(countResult.rows[0].count, 10),
464
+ indexes,
465
+ relations
466
+ };
467
+ }
468
+ async insertRow(table, data) {
469
+ if (!this.pool) throw new Error("Not connected");
470
+ const columns = Object.keys(data);
471
+ const values = Object.values(data);
472
+ const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
473
+ const sql = `INSERT INTO "${table}" (${columns.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`;
474
+ return this.query(sql, values);
475
+ }
476
+ async updateRow(table, data, where) {
477
+ if (!this.pool) throw new Error("Not connected");
478
+ const setClauses = Object.keys(data).map((c, i) => `"${c}" = $${i + 1}`).join(", ");
479
+ const whereClauses = Object.keys(where).map((c, i) => `"${c}" = $${Object.keys(data).length + i + 1}`).join(" AND ");
480
+ const values = [...Object.values(data), ...Object.values(where)];
481
+ const sql = `UPDATE "${table}" SET ${setClauses} WHERE ${whereClauses}`;
482
+ return this.query(sql, values);
483
+ }
484
+ };
485
+
486
+ // src/drivers/sqlite.ts
487
+ import { Database } from "bun:sqlite";
488
+ import fs from "fs";
489
+ var SQLiteDriver = class {
490
+ db = null;
491
+ config;
492
+ constructor(config) {
493
+ this.config = config;
494
+ }
495
+ async connect() {
496
+ if (!this.config.filepath) {
497
+ throw new Error("SQLite requires filepath");
498
+ }
499
+ if (!fs.existsSync(this.config.filepath)) {
500
+ throw new Error(`Database file not found: ${this.config.filepath}`);
501
+ }
502
+ this.db = new Database(this.config.filepath);
503
+ }
504
+ async disconnect() {
505
+ if (this.db) {
506
+ this.db.close();
507
+ this.db = null;
508
+ }
509
+ }
510
+ async query(sql, params) {
511
+ if (!this.db) throw new Error("Not connected");
512
+ const start = Date.now();
513
+ const isSelect = sql.trim().toUpperCase().startsWith("SELECT");
514
+ let result;
515
+ if (isSelect) {
516
+ const stmt = this.db.prepare(sql);
517
+ const rows = params ? stmt.all(...params) : stmt.all();
518
+ result = {
519
+ columns: rows.length > 0 ? Object.keys(rows[0]) : [],
520
+ rows,
521
+ rowCount: rows.length,
522
+ executionTimeMs: Date.now() - start
523
+ };
524
+ } else {
525
+ const stmt = this.db.prepare(sql);
526
+ const info = params ? stmt.run(...params) : stmt.run();
527
+ result = {
528
+ columns: [],
529
+ rows: [],
530
+ rowCount: 0,
531
+ affectedRows: info.changes,
532
+ executionTimeMs: Date.now() - start
533
+ };
534
+ }
535
+ return result;
536
+ }
537
+ async getTables() {
538
+ if (!this.db) throw new Error("Not connected");
539
+ const rows = this.db.prepare(
540
+ `SELECT name FROM sqlite_master
541
+ WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
542
+ ORDER BY name`
543
+ ).all();
544
+ const tables = [];
545
+ for (const row of rows) {
546
+ try {
547
+ const tableInfo = await this.getTableSchema(row.name);
548
+ tables.push(tableInfo);
549
+ } catch (error) {
550
+ console.error(`Failed to get schema for table ${row.name}:`, error);
551
+ }
552
+ }
553
+ return tables;
554
+ }
555
+ async getTableSchema(table) {
556
+ if (!this.db) throw new Error("Not connected");
557
+ const columnsRows = this.db.prepare(`PRAGMA table_info("${table}")`).all();
558
+ const columns = columnsRows.map((row) => ({
559
+ name: row.name,
560
+ type: row.type,
561
+ nullable: row.notnull === 0,
562
+ defaultValue: row.dflt_value ?? void 0,
563
+ isPrimaryKey: row.pk === 1,
564
+ isForeignKey: false
565
+ }));
566
+ const countRow = this.db.prepare(`SELECT COUNT(*) as count FROM "${table}"`).get();
567
+ const indexesRows = this.db.prepare(`PRAGMA index_list("${table}")`).all();
568
+ const indexes = await Promise.all(
569
+ indexesRows.map(async (idx) => {
570
+ const info = this.db.prepare(
571
+ `PRAGMA index_info("${idx.name}")`
572
+ ).all();
573
+ return {
574
+ name: idx.name,
575
+ unique: idx.unique === 1,
576
+ columns: info.map((i) => i.name)
577
+ };
578
+ })
579
+ );
580
+ const fkRows = this.db.prepare(`PRAGMA foreign_key_list("${table}")`).all();
581
+ const relations = fkRows.map((fk) => ({
582
+ name: `${table}_${fk.from}_fk`,
583
+ type: "belongsTo",
584
+ fromTable: table,
585
+ fromColumn: fk.from,
586
+ toTable: fk.table,
587
+ toColumn: fk.to
588
+ }));
589
+ return {
590
+ name: table,
591
+ schema: "main",
592
+ columns,
593
+ rowCount: countRow.count,
594
+ indexes,
595
+ relations
596
+ };
597
+ }
598
+ async insertRow(table, data) {
599
+ if (!this.db) throw new Error("Not connected");
600
+ const columns = Object.keys(data);
601
+ const values = Object.values(data);
602
+ const placeholders = values.map(() => "?").join(", ");
603
+ const sql = `INSERT INTO "${table}" (${columns.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`;
604
+ return this.query(sql, values);
605
+ }
606
+ async updateRow(table, data, where) {
607
+ if (!this.db) throw new Error("Not connected");
608
+ const setClauses = Object.keys(data).map((c) => `"${c}" = ?`).join(", ");
609
+ const whereClauses = Object.keys(where).map((c) => `"${c}" = ?`).join(" AND ");
610
+ const values = [...Object.values(data), ...Object.values(where)];
611
+ const sql = `UPDATE "${table}" SET ${setClauses} WHERE ${whereClauses}`;
612
+ return this.query(sql, values);
613
+ }
614
+ };
615
+
616
+ // src/drivers/index.ts
617
+ function createDriver(config) {
618
+ switch (config.type) {
619
+ case "postgresql":
620
+ return new PostgresDriver(config);
621
+ case "mysql":
622
+ return new MySQLDriver(config);
623
+ case "sqlite":
624
+ return new SQLiteDriver(config);
625
+ case "libsql":
626
+ return new LibSQLDriver(config);
627
+ default:
628
+ throw new Error(`Unsupported database type: ${config.type}`);
629
+ }
630
+ }
631
+
632
+ // src/agents/index.ts
633
+ var CONFIG_DIR = path.join(os.homedir(), ".dbstudio");
634
+ var STATUS_FILE = path.join(CONFIG_DIR, "status.json");
635
+ var Agent = class {
636
+ ws = null;
637
+ driver = null;
638
+ config;
639
+ reconnectAttempts = 0;
640
+ maxReconnectAttempts = 5;
641
+ pingInterval = null;
642
+ sshClient = null;
643
+ tunnelServer = null;
644
+ isReconnecting = false;
645
+ reconnectTimeout = null;
646
+ constructor(config) {
647
+ this.config = config;
648
+ this.ensureConfigDir();
649
+ }
650
+ ensureConfigDir() {
651
+ if (!fs2.existsSync(CONFIG_DIR)) {
652
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
653
+ }
654
+ }
655
+ async connect() {
656
+ if (this.config.dbConfig.ssh) {
657
+ await this.setupSshTunnel();
658
+ }
659
+ this.driver = createDriver(this.config.dbConfig);
660
+ await this.driver.connect();
661
+ await this.connectWebSocket();
662
+ this.saveStatus("connected");
663
+ }
664
+ async setupSshTunnel() {
665
+ const { ssh, host, port, type } = this.config.dbConfig;
666
+ if (!ssh) return;
667
+ return new Promise((resolve, reject) => {
668
+ this.sshClient = new Client();
669
+ this.sshClient.on("ready", () => {
670
+ this.tunnelServer = createServer((sock) => {
671
+ this.sshClient?.forwardOut(
672
+ "127.0.0.1",
673
+ sock.remotePort || 0,
674
+ host || "localhost",
675
+ port || (type === "postgresql" ? 5432 : 3306),
676
+ (err, stream) => {
677
+ if (err) {
678
+ sock.end();
679
+ return;
680
+ }
681
+ sock.pipe(stream).pipe(sock);
682
+ }
683
+ );
684
+ });
685
+ this.tunnelServer.listen(0, "127.0.0.1", () => {
686
+ const addr = this.tunnelServer?.address();
687
+ if (addr && typeof addr !== "string") {
688
+ this.config.dbConfig.host = "127.0.0.1";
689
+ this.config.dbConfig.port = addr.port;
690
+ console.log(
691
+ `SSH Tunnel established: 127.0.0.1:${addr.port} -> ${host}:${port || (type === "postgresql" ? 5432 : 3306)}`
692
+ );
693
+ resolve();
694
+ } else {
695
+ reject(new Error("Failed to get tunnel address"));
696
+ }
697
+ });
698
+ this.tunnelServer.on("error", reject);
699
+ }).on("error", reject).connect({
700
+ host: ssh.host,
701
+ port: ssh.port || 22,
702
+ username: ssh.username,
703
+ password: ssh.password,
704
+ privateKey: ssh.privateKey,
705
+ passphrase: ssh.passphrase
706
+ });
707
+ });
708
+ }
709
+ async connectWebSocket() {
710
+ return new Promise((resolve, reject) => {
711
+ this.ws = new WebSocket(this.config.serverUrl);
712
+ this.ws.on("open", () => {
713
+ console.log("WebSocket connected, authenticating...");
714
+ this.authenticate();
715
+ this.startPingInterval();
716
+ resolve();
717
+ });
718
+ this.ws.on("message", (data) => {
719
+ this.handleMessage(data.toString());
720
+ });
721
+ this.ws.on("close", () => {
722
+ console.log("WebSocket closed");
723
+ this.stopPingInterval();
724
+ this.handleReconnect();
725
+ });
726
+ this.ws.on("error", (error) => {
727
+ console.error("WebSocket error:", error.message);
728
+ reject(error);
729
+ });
730
+ });
731
+ }
732
+ authenticate() {
733
+ const authMessage = {
734
+ id: crypto.randomUUID(),
735
+ type: "auth",
736
+ timestamp: Date.now(),
737
+ payload: {
738
+ token: this.config.token,
739
+ connectionId: crypto.randomUUID(),
740
+ dbConfig: this.config.dbConfig
741
+ }
742
+ };
743
+ this.send(authMessage);
744
+ }
745
+ async handleMessage(data) {
746
+ try {
747
+ const message = JSON.parse(data);
748
+ switch (message.type) {
749
+ case "auth_success":
750
+ console.log("Authenticated successfully");
751
+ break;
752
+ case "auth_error":
753
+ console.error("Authentication failed:", message.payload.error);
754
+ await this.disconnect();
755
+ break;
756
+ case "ping":
757
+ this.send({ id: message.id, type: "pong", timestamp: Date.now() });
758
+ break;
759
+ case "pong":
760
+ break;
761
+ case "query":
762
+ await this.handleQuery(message);
763
+ break;
764
+ case "get_tables":
765
+ await this.handleGetTables(message);
766
+ break;
767
+ case "get_table_schema":
768
+ await this.handleGetTableSchema(message);
769
+ break;
770
+ case "insert_row":
771
+ await this.handleInsertRow(message);
772
+ break;
773
+ case "update_row":
774
+ await this.handleUpdateRow(message);
775
+ break;
776
+ default:
777
+ console.log("Unknown message type:", message.type);
778
+ }
779
+ } catch (error) {
780
+ console.error("Error handling message:", error);
781
+ }
782
+ }
783
+ async handleQuery(message) {
784
+ if (!this.driver) return;
785
+ try {
786
+ const result = await this.driver.query(
787
+ message.payload.sql,
788
+ message.payload.params
789
+ );
790
+ this.send({
791
+ id: message.id,
792
+ type: "query_result",
793
+ timestamp: Date.now(),
794
+ payload: result
795
+ });
796
+ } catch (error) {
797
+ this.send({
798
+ id: message.id,
799
+ type: "query_error",
800
+ timestamp: Date.now(),
801
+ payload: {
802
+ message: error instanceof Error ? error.message : "Query failed",
803
+ code: error?.code
804
+ }
805
+ });
806
+ }
807
+ }
808
+ async handleGetTables(message) {
809
+ if (!this.driver) return;
810
+ try {
811
+ const tables = await this.driver.getTables(message.payload.schema);
812
+ this.send({
813
+ id: message.id,
814
+ type: "tables_result",
815
+ timestamp: Date.now(),
816
+ payload: { tables }
817
+ });
818
+ } catch (error) {
819
+ console.error("Error in handleGetTables:", error);
820
+ this.send({
821
+ id: message.id,
822
+ type: "query_error",
823
+ timestamp: Date.now(),
824
+ payload: {
825
+ message: error instanceof Error ? error.message : "Failed to get tables"
826
+ }
827
+ });
828
+ }
829
+ }
830
+ async handleGetTableSchema(message) {
831
+ if (!this.driver) return;
832
+ try {
833
+ const schema = await this.driver.getTableSchema(
834
+ message.payload.table,
835
+ message.payload.schema
836
+ );
837
+ this.send({
838
+ id: message.id,
839
+ type: "table_schema_result",
840
+ timestamp: Date.now(),
841
+ payload: schema
842
+ });
843
+ } catch (error) {
844
+ this.send({
845
+ id: message.id,
846
+ type: "query_error",
847
+ timestamp: Date.now(),
848
+ payload: {
849
+ message: error instanceof Error ? error.message : "Failed to get schema"
850
+ }
851
+ });
852
+ }
853
+ }
854
+ async handleInsertRow(message) {
855
+ if (!this.driver) return;
856
+ try {
857
+ const result = await this.driver.insertRow(
858
+ message.payload.table,
859
+ message.payload.data
860
+ );
861
+ this.send({
862
+ id: message.id,
863
+ type: "query_result",
864
+ timestamp: Date.now(),
865
+ payload: result
866
+ });
867
+ } catch (error) {
868
+ this.send({
869
+ id: message.id,
870
+ type: "query_error",
871
+ timestamp: Date.now(),
872
+ payload: {
873
+ message: error instanceof Error ? error.message : "Failed to insert row"
874
+ }
875
+ });
876
+ }
877
+ }
878
+ async handleUpdateRow(message) {
879
+ if (!this.driver) return;
880
+ try {
881
+ const result = await this.driver.updateRow(
882
+ message.payload.table,
883
+ message.payload.data,
884
+ message.payload.where
885
+ );
886
+ this.send({
887
+ id: message.id,
888
+ type: "query_result",
889
+ timestamp: Date.now(),
890
+ payload: result
891
+ });
892
+ } catch (error) {
893
+ this.send({
894
+ id: message.id,
895
+ type: "query_error",
896
+ timestamp: Date.now(),
897
+ payload: {
898
+ message: error instanceof Error ? error.message : "Failed to update row"
899
+ }
900
+ });
901
+ }
902
+ }
903
+ send(message) {
904
+ if (this.ws?.readyState === WebSocket.OPEN) {
905
+ this.ws.send(JSON.stringify(message));
906
+ }
907
+ }
908
+ startPingInterval() {
909
+ this.pingInterval = setInterval(() => {
910
+ this.send({
911
+ id: crypto.randomUUID(),
912
+ type: "ping",
913
+ timestamp: Date.now()
914
+ });
915
+ }, 3e4);
916
+ }
917
+ stopPingInterval() {
918
+ if (this.pingInterval) {
919
+ clearInterval(this.pingInterval);
920
+ this.pingInterval = null;
921
+ }
922
+ }
923
+ clearReconnect() {
924
+ if (this.reconnectTimeout) {
925
+ clearTimeout(this.reconnectTimeout);
926
+ this.reconnectTimeout = null;
927
+ }
928
+ this.isReconnecting = false;
929
+ }
930
+ async handleReconnect() {
931
+ if (this.isReconnecting) return;
932
+ this.isReconnecting = true;
933
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
934
+ console.error("Max reconnect attempts reached");
935
+ this.saveStatus("error");
936
+ this.isReconnecting = false;
937
+ return;
938
+ }
939
+ this.reconnectAttempts++;
940
+ const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 3e4);
941
+ console.log(`Reconnecting in ${delay / 1e3}s...`);
942
+ this.saveStatus("connecting");
943
+ if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
944
+ this.reconnectTimeout = setTimeout(async () => {
945
+ try {
946
+ await this.connectWebSocket();
947
+ this.reconnectAttempts = 0;
948
+ this.saveStatus("connected");
949
+ } catch (_error) {
950
+ } finally {
951
+ this.isReconnecting = false;
952
+ this.reconnectTimeout = null;
953
+ }
954
+ }, delay);
955
+ }
956
+ async disconnect() {
957
+ this.stopPingInterval();
958
+ this.clearReconnect();
959
+ if (this.ws) {
960
+ this.ws.close();
961
+ this.ws = null;
962
+ }
963
+ if (this.driver) {
964
+ await this.driver.disconnect();
965
+ this.driver = null;
966
+ }
967
+ if (this.tunnelServer) {
968
+ this.tunnelServer.close();
969
+ this.tunnelServer = null;
970
+ }
971
+ if (this.sshClient) {
972
+ this.sshClient.end();
973
+ this.sshClient = null;
974
+ }
975
+ this.saveStatus("disconnected");
976
+ this.removeStatus();
977
+ }
978
+ saveStatus(status) {
979
+ const statusData = {
980
+ status,
981
+ database: this.config.dbConfig.database || this.config.dbConfig.filepath,
982
+ type: this.config.dbConfig.type,
983
+ connectedAt: Date.now(),
984
+ pid: process.pid
985
+ };
986
+ fs2.writeFileSync(STATUS_FILE, JSON.stringify(statusData, null, 2));
987
+ }
988
+ removeStatus() {
989
+ if (fs2.existsSync(STATUS_FILE)) {
990
+ fs2.unlinkSync(STATUS_FILE);
991
+ }
992
+ }
993
+ };
994
+
995
+ // src/commands/connect.ts
996
+ var connectCommand = new Command("connect").description("Connect to a database and bridge to DBStudio cloud").requiredOption("-t, --token <token>", "DBStudio connection token").option("--type <type>", "Database type: postgresql, mysql, sqlite, libsql").option("-h, --host <host>", "Database host", "localhost").option("-p, --port <port>", "Database port").option("-d, --database <database>", "Database name").option("-u, --user <user>", "Database username").option("--password <password>", "Database password").option("-a, --auth-token <token>", "Turso/libSQL auth token").option("-f, --file <filepath>", "SQLite database file path").option(
997
+ "--url <url>",
998
+ "Database connection URL (replaces host/port/db/user/pass)"
999
+ ).option("--ssh-host <host>", "SSH tunnel host").option("--ssh-port <port>", "SSH tunnel port", "22").option("--ssh-user <user>", "SSH tunnel username").option("--ssh-password <password>", "SSH tunnel password").option("--ssh-key <path>", "SSH tunnel private key path").option("--ssh-passphrase <passphrase>", "SSH tunnel private key passphrase").option("--ssl", "Enable SSL connection", false).option("--workspace <id>", "Legacy workspace ID (deprecated)", "").option(
1000
+ "--server <url>",
1001
+ "DBStudio server URL",
1002
+ process.env.DBSTUDIO_SERVER_URL || "wss://api.dbstudio.tech/ws/agent"
1003
+ ).action(async (options) => {
1004
+ let config = {
1005
+ type: options.type,
1006
+ url: options.url,
1007
+ host: options.host,
1008
+ port: options.port ? Number.parseInt(options.port, 10) : void 0,
1009
+ database: options.database,
1010
+ username: options.user,
1011
+ password: options.password,
1012
+ filepath: options.file,
1013
+ ssl: options.ssl,
1014
+ authToken: options.authToken
1015
+ };
1016
+ if (options.database?.includes("://") && !config.url) {
1017
+ config.url = options.database;
1018
+ config.database = void 0;
1019
+ }
1020
+ if (options.sshHost) {
1021
+ config.ssh = {
1022
+ host: options.sshHost,
1023
+ port: options.sshPort ? Number.parseInt(options.sshPort, 10) : 22,
1024
+ username: options.sshUser,
1025
+ password: options.sshPassword,
1026
+ privateKey: options.sshKey ? fs3.readFileSync(options.sshKey, "utf8") : void 0,
1027
+ passphrase: options.sshPassphrase
1028
+ };
1029
+ }
1030
+ const spinner = ora("Initializing connection...");
1031
+ if (!config.type && !config.database && !config.filepath && !config.url && !config.authToken) {
1032
+ console.log(chalk.cyan("Welcome to DBStudio CLI Setup!"));
1033
+ console.log(chalk.gray("Let's connect your database.\n"));
1034
+ const response = await prompts(
1035
+ [
1036
+ {
1037
+ type: "select",
1038
+ name: "type",
1039
+ message: "Database Type",
1040
+ choices: [
1041
+ { title: "PostgreSQL", value: "postgresql" },
1042
+ { title: "MySQL", value: "mysql" },
1043
+ { title: "SQLite", value: "sqlite" },
1044
+ { title: "Turso (libSQL)", value: "libsql" }
1045
+ ]
1046
+ },
1047
+ {
1048
+ type: (prev) => {
1049
+ if (prev === "sqlite") return "text";
1050
+ if (prev === "libsql") return null;
1051
+ return "select";
1052
+ },
1053
+ name: "methodOrPath",
1054
+ message: (_prev, values) => values.type === "sqlite" ? "Database File Path" : "Connection Method",
1055
+ choices: (_prev, values) => values.type === "sqlite" ? [] : [
1056
+ { title: "Host / Port / Database", value: "params" },
1057
+ { title: "Connection URL", value: "url" }
1058
+ ],
1059
+ initial: (_prev, values) => values.type === "sqlite" ? "./local.db" : 0
1060
+ },
1061
+ {
1062
+ type: (_prev, values) => values.type === "libsql" || values.methodOrPath === "url" ? "text" : null,
1063
+ name: "url",
1064
+ message: (_prev, values) => values.type === "libsql" ? "libSQL/Turso URL" : "Connection URL",
1065
+ initial: (_prev, values) => values.type === "libsql" ? "libsql://[your-db].turso.io" : values.type === "postgresql" ? "postgresql://user:pass@localhost:5432/db" : "mysql://user:pass@localhost:3306/db"
1066
+ },
1067
+ {
1068
+ type: (_prev, values) => values.type === "libsql" ? "text" : null,
1069
+ name: "authToken",
1070
+ message: "Auth Token (optional for local libsql)"
1071
+ },
1072
+ {
1073
+ type: (_prev, values) => values.type !== "sqlite" && values.type !== "libsql" && values.methodOrPath === "params" ? "text" : null,
1074
+ name: "host",
1075
+ message: "Host",
1076
+ initial: "localhost"
1077
+ },
1078
+ {
1079
+ type: (_prev, values) => values.type !== "sqlite" && values.type !== "libsql" && values.methodOrPath === "params" ? "number" : null,
1080
+ name: "port",
1081
+ message: "Port",
1082
+ initial: (_prev, values) => values.type === "postgresql" ? 5432 : 3306
1083
+ },
1084
+ {
1085
+ type: (_prev, values) => values.type !== "sqlite" && values.type !== "libsql" && values.methodOrPath === "params" ? "text" : null,
1086
+ name: "database",
1087
+ message: "Database Name"
1088
+ },
1089
+ {
1090
+ type: (_prev, values) => values.type !== "sqlite" && values.type !== "libsql" && values.methodOrPath === "params" ? "text" : null,
1091
+ name: "username",
1092
+ message: "Username"
1093
+ },
1094
+ {
1095
+ type: (_prev, values) => values.type !== "sqlite" && values.type !== "libsql" && values.methodOrPath === "params" ? "password" : null,
1096
+ name: "password",
1097
+ message: "Password"
1098
+ },
1099
+ {
1100
+ type: (_prev, values) => values.type !== "sqlite" && values.type !== "libsql" ? "confirm" : null,
1101
+ name: "useSsh",
1102
+ message: "Use SSH Tunnel?",
1103
+ initial: false
1104
+ },
1105
+ {
1106
+ type: (_prev, values) => values.useSsh ? "text" : null,
1107
+ name: "sshHost",
1108
+ message: "SSH Host"
1109
+ },
1110
+ {
1111
+ type: (_prev, values) => values.useSsh ? "number" : null,
1112
+ name: "sshPort",
1113
+ message: "SSH Port",
1114
+ initial: 22
1115
+ },
1116
+ {
1117
+ type: (_prev, values) => values.useSsh ? "text" : null,
1118
+ name: "sshUser",
1119
+ message: "SSH Username"
1120
+ },
1121
+ {
1122
+ type: (_prev, values) => values.useSsh ? "select" : null,
1123
+ name: "sshAuthType",
1124
+ message: "SSH Auth Type",
1125
+ choices: [
1126
+ { title: "Password", value: "password" },
1127
+ { title: "Private Key", value: "key" }
1128
+ ]
1129
+ },
1130
+ {
1131
+ type: (_prev, values) => values.sshAuthType === "password" ? "password" : null,
1132
+ name: "sshPassword",
1133
+ message: "SSH Password"
1134
+ },
1135
+ {
1136
+ type: (_prev, values) => values.sshAuthType === "key" ? "text" : null,
1137
+ name: "sshKeyPath",
1138
+ message: "SSH Key Path",
1139
+ initial: "~/.ssh/id_rsa"
1140
+ },
1141
+ {
1142
+ type: (_prev, values) => values.type !== "sqlite" && values.type !== "libsql" ? "confirm" : null,
1143
+ name: "ssl",
1144
+ message: "Enable SSL?",
1145
+ initial: false
1146
+ }
1147
+ ],
1148
+ {
1149
+ onCancel: () => {
1150
+ console.log(chalk.yellow("\nSetup cancelled."));
1151
+ process.exit(0);
1152
+ }
1153
+ }
1154
+ );
1155
+ config = {
1156
+ type: response.type,
1157
+ url: response.url,
1158
+ host: response.host,
1159
+ port: response.port,
1160
+ database: response.database,
1161
+ username: response.username,
1162
+ password: response.password,
1163
+ filepath: response.methodOrPath,
1164
+ // If type was sqlite, methodOrPath holds the filepath
1165
+ authToken: response.authToken,
1166
+ ssl: response.ssl
1167
+ };
1168
+ if (response.type === "sqlite") {
1169
+ config.filepath = response.methodOrPath;
1170
+ }
1171
+ if (response.useSsh) {
1172
+ try {
1173
+ config.ssh = {
1174
+ host: response.sshHost,
1175
+ port: response.sshPort,
1176
+ username: response.sshUser,
1177
+ password: response.sshPassword,
1178
+ privateKey: response.sshKeyPath ? fs3.readFileSync(response.sshKeyPath, "utf8") : void 0
1179
+ };
1180
+ } catch (err) {
1181
+ spinner.fail(
1182
+ chalk.red(
1183
+ `Failed to read SSH key: ${err instanceof Error ? err.message : String(err)}`
1184
+ )
1185
+ );
1186
+ process.exit(1);
1187
+ }
1188
+ }
1189
+ }
1190
+ if (config.type !== "sqlite" && config.type !== "libsql" && !config.url && config.host && !config.password) {
1191
+ const response = await prompts({
1192
+ type: "password",
1193
+ name: "password",
1194
+ message: `Password for ${config.username || "user"}@${config.host}`
1195
+ });
1196
+ if (response.password) {
1197
+ config.password = response.password;
1198
+ }
1199
+ }
1200
+ spinner.start();
1201
+ try {
1202
+ if (!["postgresql", "mysql", "sqlite", "libsql"].includes(config.type)) {
1203
+ spinner.fail(chalk.red(`Invalid database type: ${config.type}`));
1204
+ process.exit(1);
1205
+ }
1206
+ if (config.type !== "sqlite" && config.type !== "libsql" && !config.port && !config.url) {
1207
+ config.port = config.type === "postgresql" ? 5432 : 3306;
1208
+ }
1209
+ if (config.type === "sqlite" && !config.filepath) {
1210
+ spinner.fail(chalk.red("SQLite requires --file option or input"));
1211
+ process.exit(1);
1212
+ }
1213
+ if (config.type === "libsql" && !config.url) {
1214
+ spinner.fail(chalk.red("libSQL requires a connection URL"));
1215
+ process.exit(1);
1216
+ }
1217
+ if (config.type !== "sqlite" && config.type !== "libsql" && !config.database && !config.url) {
1218
+ spinner.fail(chalk.red("Database name or URL is required"));
1219
+ process.exit(1);
1220
+ }
1221
+ spinner.text = "Connecting to database...";
1222
+ const agent = new Agent({
1223
+ serverUrl: options.server,
1224
+ token: options.token,
1225
+ dbConfig: config
1226
+ });
1227
+ await agent.connect();
1228
+ spinner.succeed(chalk.green("Connected to DBStudio!"));
1229
+ console.log(
1230
+ chalk.gray("\nAgent is running. Press Ctrl+C to disconnect.\n")
1231
+ );
1232
+ let cmd = `dbstudio connect --token "${options.token}" --type ${config.type}`;
1233
+ if (config.url) {
1234
+ cmd += ` --url "${config.url}"`;
1235
+ }
1236
+ if (config.authToken) {
1237
+ cmd += ` --auth-token "${config.authToken}"`;
1238
+ }
1239
+ if (config.type === "sqlite") {
1240
+ cmd += ` --file "${config.filepath}"`;
1241
+ }
1242
+ if (!config.url && config.type !== "sqlite" && config.type !== "libsql") {
1243
+ if (config.host) cmd += ` --host "${config.host}"`;
1244
+ if (config.port) cmd += ` --port ${config.port}`;
1245
+ if (config.database) cmd += ` --database "${config.database}"`;
1246
+ if (config.username) cmd += ` --user "${config.username}"`;
1247
+ if (config.password) cmd += ` --password "${config.password}"`;
1248
+ if (config.ssl) cmd += " --ssl";
1249
+ }
1250
+ if (config.ssh) {
1251
+ cmd += ` --ssh-host "${config.ssh.host}" --ssh-user "${config.ssh.username}"`;
1252
+ if (config.ssh.port !== 22) cmd += ` --ssh-port ${config.ssh.port}`;
1253
+ }
1254
+ console.log(chalk.gray("To skip prompts next time, run:"));
1255
+ console.log(chalk.cyan(`${cmd}
1256
+ `));
1257
+ console.log(
1258
+ chalk.cyan("Database:"),
1259
+ config.database || config.filepath || config.url
1260
+ );
1261
+ console.log(chalk.cyan("Type:"), config.type);
1262
+ console.log(chalk.cyan("Server:"), options.server);
1263
+ process.on("SIGINT", async () => {
1264
+ console.log(chalk.yellow("\n\nDisconnecting..."));
1265
+ await agent.disconnect();
1266
+ console.log(chalk.green("Disconnected."));
1267
+ process.exit(0);
1268
+ });
1269
+ await new Promise(() => {
1270
+ });
1271
+ } catch (error) {
1272
+ spinner.fail(chalk.red("Connection failed"));
1273
+ console.error(
1274
+ chalk.red(error instanceof Error ? error.message : "Unknown error")
1275
+ );
1276
+ process.exit(1);
1277
+ }
1278
+ });
1279
+
1280
+ // src/commands/disconnect.ts
1281
+ import fs4 from "fs";
1282
+ import os2 from "os";
1283
+ import path2 from "path";
1284
+ import chalk2 from "chalk";
1285
+ import { Command as Command2 } from "commander";
1286
+ var CONFIG_DIR2 = path2.join(os2.homedir(), ".dbstudio");
1287
+ var STATUS_FILE2 = path2.join(CONFIG_DIR2, "status.json");
1288
+ var disconnectCommand = new Command2("disconnect").description("Disconnect the running DBStudio agent").action(() => {
1289
+ try {
1290
+ if (!fs4.existsSync(STATUS_FILE2)) {
1291
+ console.log(chalk2.yellow("No active connection to disconnect."));
1292
+ return;
1293
+ }
1294
+ const status = JSON.parse(fs4.readFileSync(STATUS_FILE2, "utf-8"));
1295
+ if (status.pid) {
1296
+ try {
1297
+ process.kill(status.pid, "SIGINT");
1298
+ console.log(chalk2.green("Sent disconnect signal to agent."));
1299
+ } catch (_error) {
1300
+ console.log(chalk2.yellow("Agent process not found. Cleaning up..."));
1301
+ }
1302
+ }
1303
+ fs4.unlinkSync(STATUS_FILE2);
1304
+ console.log(chalk2.green("Disconnected successfully."));
1305
+ } catch (error) {
1306
+ console.log(chalk2.red("Error disconnecting"));
1307
+ console.error(error instanceof Error ? error.message : "Unknown error");
1308
+ }
1309
+ });
1310
+
1311
+ // src/commands/status.ts
1312
+ import fs5 from "fs";
1313
+ import os3 from "os";
1314
+ import path3 from "path";
1315
+ import chalk3 from "chalk";
1316
+ import { Command as Command3 } from "commander";
1317
+ var CONFIG_DIR3 = path3.join(os3.homedir(), ".dbstudio");
1318
+ var STATUS_FILE3 = path3.join(CONFIG_DIR3, "status.json");
1319
+ var statusCommand = new Command3("status").description("Check the status of the DBStudio agent").action(() => {
1320
+ try {
1321
+ if (!fs5.existsSync(STATUS_FILE3)) {
1322
+ console.log(chalk3.yellow("No active connection."));
1323
+ console.log(chalk3.gray("Run 'dbstudio connect' to start an agent."));
1324
+ return;
1325
+ }
1326
+ const status = JSON.parse(fs5.readFileSync(STATUS_FILE3, "utf-8"));
1327
+ console.log(chalk3.cyan("Connection Status"));
1328
+ console.log(chalk3.gray("\u2500".repeat(40)));
1329
+ console.log(chalk3.white("Status:"), getStatusBadge(status.status));
1330
+ console.log(chalk3.white("Database:"), status.database);
1331
+ console.log(chalk3.white("Type:"), status.type);
1332
+ console.log(
1333
+ chalk3.white("Connected at:"),
1334
+ new Date(status.connectedAt).toLocaleString()
1335
+ );
1336
+ if (status.pid) {
1337
+ console.log(chalk3.white("Process ID:"), status.pid);
1338
+ }
1339
+ } catch (_error) {
1340
+ console.log(chalk3.red("Error reading status"));
1341
+ console.log(chalk3.gray("Run 'dbstudio connect' to start an agent."));
1342
+ }
1343
+ });
1344
+ function getStatusBadge(status) {
1345
+ switch (status) {
1346
+ case "connected":
1347
+ return chalk3.green("\u25CF Connected");
1348
+ case "connecting":
1349
+ return chalk3.yellow("\u25D0 Connecting...");
1350
+ case "disconnected":
1351
+ return chalk3.gray("\u25CB Disconnected");
1352
+ case "error":
1353
+ return chalk3.red("\u2715 Error");
1354
+ default:
1355
+ return chalk3.gray("Unknown");
1356
+ }
1357
+ }
1358
+
1359
+ // src/index.ts
1360
+ var logo = `
1361
+ ${chalk4.green("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")}
1362
+ ${chalk4.green("\u2551")} ${chalk4.bold.white("DBStudio")} ${chalk4.gray("CLI Agent")} ${chalk4.green("\u2551")}
1363
+ ${chalk4.green("\u2551")} ${chalk4.gray("Connect your database to the cloud")} ${chalk4.green("\u2551")}
1364
+ ${chalk4.green("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")}
1365
+ `;
1366
+ program.name("dbstudio").description("DBStudio CLI - Connect local databases to DBStudio cloud").version("0.1.0").hook("preAction", () => {
1367
+ console.log(logo);
1368
+ });
1369
+ program.addCommand(connectCommand);
1370
+ program.addCommand(statusCommand);
1371
+ program.addCommand(disconnectCommand);
1372
+ program.parse(process.argv);