@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.d.ts +2 -0
- package/dist/index.js +1372 -0
- package/package.json +36 -0
- package/src/agents/index.ts +458 -0
- package/src/commands/connect.ts +418 -0
- package/src/commands/disconnect.ts +37 -0
- package/src/commands/status.ts +54 -0
- package/src/drivers/index.ts +58 -0
- package/src/drivers/libsql.ts +189 -0
- package/src/drivers/mysql.ts +199 -0
- package/src/drivers/postgres.ts +224 -0
- package/src/drivers/sqlite.ts +206 -0
- package/src/index.ts +28 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +11 -0
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);
|