@dbx-app/cli 0.4.34 → 0.4.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -9
- package/bin/dbx.js +44 -0
- package/package.json +14 -14
- package/dist/cli-format.d.ts +0 -20
- package/dist/cli-format.js +0 -41
- package/dist/cli.d.ts +0 -15
- package/dist/cli.js +0 -352
package/README.md
CHANGED
|
@@ -17,7 +17,31 @@ brew tap t8y2/dbx
|
|
|
17
17
|
brew install dbx-cli
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
The npm package installs the native CLI for the current platform automatically. Node.js 18.18.0 or newer is only needed for the npm launcher; direct native distributions do not require Node.js.
|
|
21
|
+
|
|
22
|
+
### Native downloads
|
|
23
|
+
|
|
24
|
+
The `packages-v*` GitHub Release also provides standalone native CLI archives:
|
|
25
|
+
|
|
26
|
+
| Platform | Archive |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| macOS Apple Silicon | `dbx-cli-darwin-arm64.tar.gz` |
|
|
29
|
+
| macOS Intel | `dbx-cli-darwin-x64.tar.gz` |
|
|
30
|
+
| Linux glibc ARM64 | `dbx-cli-linux-arm64-gnu.tar.gz` |
|
|
31
|
+
| Linux glibc x64 | `dbx-cli-linux-x64-gnu.tar.gz` |
|
|
32
|
+
| Windows ARM64 | `dbx-cli-win32-arm64.zip` |
|
|
33
|
+
| Windows x64 | `dbx-cli-win32-x64.zip` |
|
|
34
|
+
|
|
35
|
+
Verify the downloaded archive with `CLI-SHA256SUMS`, extract it, and run the native binary directly:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
tar -xzf dbx-cli-linux-x64-gnu.tar.gz
|
|
39
|
+
chmod +x dbx
|
|
40
|
+
./dbx --version
|
|
41
|
+
./dbx connections list --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Standalone binaries do not require Node.js. They read the same DBX connection storage as the desktop application; set `DBX_DATA_DIR` when using a custom or portable data directory.
|
|
21
45
|
|
|
22
46
|
## Usage
|
|
23
47
|
|
|
@@ -90,23 +114,19 @@ Some CLI commands can run without DBX Desktop:
|
|
|
90
114
|
- `query`
|
|
91
115
|
- `context`
|
|
92
116
|
|
|
93
|
-
Direct execution
|
|
117
|
+
Direct execution supports PostgreSQL/Redshift, MySQL-compatible databases (MySQL, Doris, StarRocks), and SQLite. Other database types use the DBX Desktop bridge or DBX Agent/JDBC infrastructure.
|
|
94
118
|
|
|
95
119
|
Use `dbx doctor` to check whether the DBX connection database, connection table, native SQLite loader, and desktop bridge are available. Use `dbx capabilities` to list direct-query and bridge-required database types.
|
|
96
120
|
|
|
97
|
-
If
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
pnpm rebuild better-sqlite3 keytar --pending
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
For global npm installs, reinstall the CLI with the same Node.js version:
|
|
121
|
+
If the optional platform package was not installed, reinstall without `--no-optional`:
|
|
104
122
|
|
|
105
123
|
```bash
|
|
106
124
|
npm uninstall -g @dbx-app/cli
|
|
107
125
|
npm install -g @dbx-app/cli
|
|
108
126
|
```
|
|
109
127
|
|
|
128
|
+
The native CLI does not require `better-sqlite3` and is not coupled to the Node.js ABI.
|
|
129
|
+
|
|
110
130
|
## Error Codes
|
|
111
131
|
|
|
112
132
|
CLI JSON errors use stable codes:
|
package/bin/dbx.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const platformPackages = {
|
|
10
|
+
"darwin-arm64": "@dbx-app/cli-darwin-arm64",
|
|
11
|
+
"darwin-x64": "@dbx-app/cli-darwin-x64",
|
|
12
|
+
"linux-arm64": "@dbx-app/cli-linux-arm64-gnu",
|
|
13
|
+
"linux-x64": "@dbx-app/cli-linux-x64-gnu",
|
|
14
|
+
"win32-arm64": "@dbx-app/cli-win32-arm64",
|
|
15
|
+
"win32-x64": "@dbx-app/cli-win32-x64",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
19
|
+
const packageName = platformPackages[platformKey];
|
|
20
|
+
if (!packageName) {
|
|
21
|
+
console.error(`Unsupported platform: ${platformKey}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let binary;
|
|
26
|
+
try {
|
|
27
|
+
const packageJson = require.resolve(`${packageName}/package.json`);
|
|
28
|
+
binary = join(dirname(packageJson), "bin", process.platform === "win32" ? "dbx.exe" : "dbx");
|
|
29
|
+
} catch {
|
|
30
|
+
console.error(`The optional package ${packageName} was not installed. Reinstall @dbx-app/cli without --no-optional.`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!existsSync(binary)) {
|
|
35
|
+
console.error(`DBX CLI binary was not found at ${binary}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit", env: process.env });
|
|
40
|
+
if (result.error) {
|
|
41
|
+
console.error(result.error.message);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
process.exit(result.status ?? 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dbx-app/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.37",
|
|
4
4
|
"description": "Command line interface for DBX database connections, schema, and safe queries",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agent",
|
|
@@ -21,26 +21,26 @@
|
|
|
21
21
|
"directory": "packages/cli"
|
|
22
22
|
},
|
|
23
23
|
"bin": {
|
|
24
|
-
"dbx": "
|
|
24
|
+
"dbx": "bin/dbx.js"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
-
"
|
|
27
|
+
"bin"
|
|
28
28
|
],
|
|
29
29
|
"type": "module",
|
|
30
|
-
"
|
|
31
|
-
"@dbx-app/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"@
|
|
35
|
-
"
|
|
36
|
-
"
|
|
30
|
+
"optionalDependencies": {
|
|
31
|
+
"@dbx-app/cli-darwin-arm64": "0.4.37",
|
|
32
|
+
"@dbx-app/cli-darwin-x64": "0.4.37",
|
|
33
|
+
"@dbx-app/cli-linux-arm64-gnu": "0.4.37",
|
|
34
|
+
"@dbx-app/cli-linux-x64-gnu": "0.4.37",
|
|
35
|
+
"@dbx-app/cli-win32-arm64": "0.4.37",
|
|
36
|
+
"@dbx-app/cli-win32-x64": "0.4.37"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
|
-
"node": ">=
|
|
39
|
+
"node": ">=18.18.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"start": "
|
|
43
|
-
"test": "
|
|
44
|
-
"build": "
|
|
42
|
+
"start": "cargo run -p dbx-cli --no-default-features --",
|
|
43
|
+
"test": "cargo test -p dbx-cli --no-default-features",
|
|
44
|
+
"build": "cargo build -p dbx-cli --release --no-default-features"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/dist/cli-format.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { formatCell, mdTable, type ConnectionConfig } from "@dbx-app/node-core";
|
|
2
|
-
export { formatCell, mdTable };
|
|
3
|
-
export interface ConnectionSummary {
|
|
4
|
-
name: string;
|
|
5
|
-
type: string;
|
|
6
|
-
host: string;
|
|
7
|
-
port: number;
|
|
8
|
-
database?: string;
|
|
9
|
-
}
|
|
10
|
-
export declare function connectionSummary(connection: ConnectionConfig): ConnectionSummary;
|
|
11
|
-
export interface ErrorPayload {
|
|
12
|
-
error: {
|
|
13
|
-
code: string;
|
|
14
|
-
message: string;
|
|
15
|
-
hint?: string;
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
export declare function errorPayload(code: string, message: string): ErrorPayload;
|
|
19
|
-
export declare function formatErrorMessage(code: string, message: string): string;
|
|
20
|
-
export declare function csvTable<T extends object>(headers: string[], rows: T[]): string;
|
package/dist/cli-format.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { formatCell, mdTable } from "@dbx-app/node-core";
|
|
2
|
-
export { formatCell, mdTable };
|
|
3
|
-
export function connectionSummary(connection) {
|
|
4
|
-
return {
|
|
5
|
-
name: connection.name,
|
|
6
|
-
type: connection.db_type,
|
|
7
|
-
host: connection.host,
|
|
8
|
-
port: connection.port,
|
|
9
|
-
database: connection.database || undefined,
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
export function errorPayload(code, message) {
|
|
13
|
-
const hint = errorHint(code, message);
|
|
14
|
-
return { error: hint ? { code, message, hint } : { code, message } };
|
|
15
|
-
}
|
|
16
|
-
export function formatErrorMessage(code, message) {
|
|
17
|
-
const hint = errorHint(code, message);
|
|
18
|
-
return hint ? `${message}\n\nHint: ${hint}` : message;
|
|
19
|
-
}
|
|
20
|
-
function errorHint(code, message) {
|
|
21
|
-
if (code === "CONNECTION_STORE_ERROR" && /NODE_MODULE_VERSION|compiled against a different Node\.js version/i.test(message)) {
|
|
22
|
-
return "Rebuild DBX CLI native dependencies with your active Node.js: pnpm rebuild better-sqlite3 keytar --pending, or reinstall the package with the same Node.js version you use to run dbx.";
|
|
23
|
-
}
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
export function csvTable(headers, rows) {
|
|
27
|
-
const lines = [headers.map(csvCell).join(",")];
|
|
28
|
-
for (const row of rows) {
|
|
29
|
-
const values = row;
|
|
30
|
-
lines.push(headers.map((header) => csvCell(values[header])).join(","));
|
|
31
|
-
}
|
|
32
|
-
return `${lines.join("\n")}\n`;
|
|
33
|
-
}
|
|
34
|
-
function csvCell(value) {
|
|
35
|
-
if (value === null || value === undefined)
|
|
36
|
-
return "";
|
|
37
|
-
const text = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
38
|
-
if (/[",\r\n]/.test(text))
|
|
39
|
-
return `"${text.replace(/"/g, '""')}"`;
|
|
40
|
-
return text;
|
|
41
|
-
}
|
package/dist/cli.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { type Backend, type DbxDiagnostics } from "@dbx-app/node-core";
|
|
3
|
-
export interface CliResult {
|
|
4
|
-
exitCode: number;
|
|
5
|
-
stdout: string;
|
|
6
|
-
stderr: string;
|
|
7
|
-
}
|
|
8
|
-
interface RunOptions {
|
|
9
|
-
backend?: Backend;
|
|
10
|
-
backendFactory?: (env?: NodeJS.ProcessEnv) => Promise<Backend>;
|
|
11
|
-
env?: NodeJS.ProcessEnv;
|
|
12
|
-
diagnostics?: () => Promise<DbxDiagnostics>;
|
|
13
|
-
}
|
|
14
|
-
export declare function runCli(argv: string[], options?: RunOptions): Promise<CliResult>;
|
|
15
|
-
export {};
|
package/dist/cli.js
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { buildSchemaContext, createBackend, DIRECT_QUERY_TYPES, BRIDGE_REQUIRED_TYPES, evaluateSqlSafety, formatSchemaContext, getDbxDiagnostics, isMainModule, postBridge, supportsHashLineComments } from "@dbx-app/node-core";
|
|
4
|
-
import { connectionSummary, csvTable, errorPayload, formatCell, formatErrorMessage, mdTable } from "./cli-format.js";
|
|
5
|
-
class CliError extends Error {
|
|
6
|
-
code;
|
|
7
|
-
constructor(code, message) {
|
|
8
|
-
super(message);
|
|
9
|
-
this.code = code;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
export async function runCli(argv, options = {}) {
|
|
13
|
-
const env = options.env ?? process.env;
|
|
14
|
-
let ownedBackend;
|
|
15
|
-
try {
|
|
16
|
-
const flags = parseFlags(argv);
|
|
17
|
-
const args = flags.args;
|
|
18
|
-
if (flags.version) {
|
|
19
|
-
return ok(`${await packageVersion()}\n`);
|
|
20
|
-
}
|
|
21
|
-
if (args.length === 0 || flags.help || args[0] === "help") {
|
|
22
|
-
return ok(`${usage()}\n`);
|
|
23
|
-
}
|
|
24
|
-
const backendFactory = options.backendFactory ?? createBackend;
|
|
25
|
-
const backend = options.backend ?? (ownedBackend = await backendFactory(env));
|
|
26
|
-
if (args[0] === "doctor") {
|
|
27
|
-
ensureArgCount(args, 1, "dbx doctor");
|
|
28
|
-
const diagnostics = await (options.diagnostics ?? getDbxDiagnostics)();
|
|
29
|
-
if (flags.format === "json")
|
|
30
|
-
return okJson(diagnostics);
|
|
31
|
-
if (flags.format === "csv") {
|
|
32
|
-
return ok(csvTable(["check", "value"], [
|
|
33
|
-
{ check: "appDataDir", value: diagnostics.appDataDir },
|
|
34
|
-
{ check: "dbPath", value: diagnostics.dbPath },
|
|
35
|
-
{ check: "dbPathExists", value: diagnostics.dbPathExists },
|
|
36
|
-
{ check: "connectionsTableExists", value: diagnostics.connectionsTableExists },
|
|
37
|
-
{ check: "connectionRowCount", value: diagnostics.connectionRowCount },
|
|
38
|
-
{ check: "loadConnectionsOk", value: diagnostics.loadConnectionsOk },
|
|
39
|
-
{ check: "loadedConnectionCount", value: diagnostics.loadedConnectionCount },
|
|
40
|
-
{ check: "loadConnectionsError", value: diagnostics.loadConnectionsError ?? "" },
|
|
41
|
-
{ check: "loadConnectionsHint", value: diagnostics.loadConnectionsHint ?? "" },
|
|
42
|
-
{ check: "bridgePortFile", value: diagnostics.bridgePortFile },
|
|
43
|
-
{ check: "bridgePortFileExists", value: diagnostics.bridgePortFileExists },
|
|
44
|
-
{ check: "bridgeUrl", value: diagnostics.bridgeUrl ?? "" },
|
|
45
|
-
]));
|
|
46
|
-
}
|
|
47
|
-
return ok(formatDoctor(diagnostics));
|
|
48
|
-
}
|
|
49
|
-
if (args[0] === "capabilities") {
|
|
50
|
-
ensureArgCount(args, 1, "dbx capabilities");
|
|
51
|
-
const payload = {
|
|
52
|
-
directQueryTypes: [...DIRECT_QUERY_TYPES],
|
|
53
|
-
bridgeRequiredTypes: [...BRIDGE_REQUIRED_TYPES],
|
|
54
|
-
};
|
|
55
|
-
if (flags.format === "json")
|
|
56
|
-
return okJson(payload);
|
|
57
|
-
if (flags.format === "csv") {
|
|
58
|
-
return ok(csvTable(["mode", "type"], [...payload.directQueryTypes.map((type) => ({ mode: "direct", type })), ...payload.bridgeRequiredTypes.map((type) => ({ mode: "bridge", type }))]));
|
|
59
|
-
}
|
|
60
|
-
return ok(`${mdTable(["Mode", "Types"], [
|
|
61
|
-
["Direct", payload.directQueryTypes.join(", ")],
|
|
62
|
-
["Requires DBX Desktop", payload.bridgeRequiredTypes.join(", ")],
|
|
63
|
-
])}\n`);
|
|
64
|
-
}
|
|
65
|
-
if (args[0] === "connections" && args[1] === "list") {
|
|
66
|
-
ensureArgCount(args, 2, "dbx connections list");
|
|
67
|
-
const connections = (await backend.loadConnections()).map(connectionSummary);
|
|
68
|
-
if (flags.format === "json")
|
|
69
|
-
return okJson({ connections });
|
|
70
|
-
if (flags.format === "csv")
|
|
71
|
-
return ok(csvTable(["name", "type", "host", "port", "database"], connections));
|
|
72
|
-
return ok(`${mdTable(["Name", "Type", "Host", "Port", "Database"], connections.map((c) => [c.name, c.type, c.host, String(c.port), c.database ?? ""]))}\n`);
|
|
73
|
-
}
|
|
74
|
-
if (args[0] === "schema" && args[1] === "list") {
|
|
75
|
-
ensureArgCount(args, 3, "dbx schema list");
|
|
76
|
-
const connectionName = required(args[2], "Connection name is required.");
|
|
77
|
-
const config = await findConnectionOrThrow(backend, connectionName);
|
|
78
|
-
const tables = await backend.listTables(config, flags.schema);
|
|
79
|
-
if (flags.format === "json")
|
|
80
|
-
return okJson({ connection: connectionName, schema: flags.schema, tables });
|
|
81
|
-
if (flags.format === "csv")
|
|
82
|
-
return ok(csvTable(["name", "type"], tables));
|
|
83
|
-
return ok(`${mdTable(["Table", "Type"], tables.map((t) => [t.name, t.type]))}\n`);
|
|
84
|
-
}
|
|
85
|
-
if (args[0] === "schema" && args[1] === "describe") {
|
|
86
|
-
ensureArgCount(args, 4, "dbx schema describe");
|
|
87
|
-
const connectionName = required(args[2], "Connection name is required.");
|
|
88
|
-
const table = required(args[3], "Table name is required.");
|
|
89
|
-
const config = await findConnectionOrThrow(backend, connectionName);
|
|
90
|
-
const columns = await backend.describeTable(config, table, flags.schema);
|
|
91
|
-
if (flags.format === "json")
|
|
92
|
-
return okJson({ connection: connectionName, schema: flags.schema, table, columns });
|
|
93
|
-
if (flags.format === "csv") {
|
|
94
|
-
return ok(csvTable(["name", "data_type", "is_nullable", "is_primary_key", "column_default", "comment"], columns));
|
|
95
|
-
}
|
|
96
|
-
return ok(`${mdTable(["Column", "Type", "Nullable", "Default", "Comment"], columns.map((c) => [c.is_primary_key ? `${c.name} (PK)` : c.name, c.data_type, c.is_nullable ? "YES" : "NO", c.column_default ?? "", c.comment ?? ""]))}\n`);
|
|
97
|
-
}
|
|
98
|
-
if (args[0] === "query") {
|
|
99
|
-
const usesDefaultConnection = !!env.DBX_CONNECTION && args.length === (flags.file ? 1 : 2);
|
|
100
|
-
ensureArgCount(args, usesDefaultConnection ? (flags.file ? 1 : 2) : flags.file ? 2 : 3, "dbx query");
|
|
101
|
-
const connectionName = usesDefaultConnection ? env.DBX_CONNECTION : required(args[1], "Connection name is required.");
|
|
102
|
-
if (flags.file && args[2]) {
|
|
103
|
-
throw new CliError("INVALID_ARGUMENT", "Provide SQL either inline or with --file, not both.");
|
|
104
|
-
}
|
|
105
|
-
const sqlArg = usesDefaultConnection ? args[1] : args[2];
|
|
106
|
-
const sql = flags.file ? await readFile(flags.file, "utf-8") : required(sqlArg, "SQL string or --file is required.");
|
|
107
|
-
const config = await findConnectionOrThrow(backend, connectionName);
|
|
108
|
-
const envSafety = sqlSafetyFromCliEnv(env);
|
|
109
|
-
if (flags.allowDangerous && !flags.allowWrites && !envSafety.allowWrites) {
|
|
110
|
-
throw new CliError("INVALID_OPTION", "--allow-dangerous-sql requires --allow-writes.");
|
|
111
|
-
}
|
|
112
|
-
const safetyOptions = {
|
|
113
|
-
allowWrites: flags.allowWrites || envSafety.allowWrites,
|
|
114
|
-
allowDangerous: flags.allowDangerous || envSafety.allowDangerous,
|
|
115
|
-
hashLineComments: supportsHashLineComments(config.db_type),
|
|
116
|
-
};
|
|
117
|
-
const safety = evaluateSqlSafety(sql, safetyOptions);
|
|
118
|
-
if (!safety.allowed)
|
|
119
|
-
return fail("SQL_BLOCKED", safety.reason ?? "SQL blocked.", flags.json);
|
|
120
|
-
const result = await backend.executeQuery(config, sql, { maxRows: flags.maxRows, timeoutMs: flags.timeoutMs });
|
|
121
|
-
if (flags.format === "json") {
|
|
122
|
-
return okJson({ connection: connectionName, columns: result.columns, rows: result.rows, row_count: result.row_count });
|
|
123
|
-
}
|
|
124
|
-
if (flags.format === "csv")
|
|
125
|
-
return ok(csvTable(result.columns, result.rows));
|
|
126
|
-
if (result.columns.length === 0)
|
|
127
|
-
return ok(`Query executed. ${result.row_count} row(s) affected.\n`);
|
|
128
|
-
return ok(`${mdTable(result.columns, result.rows.map((row) => result.columns.map((column) => formatCell(row[column]))))}\n\n${result.row_count} row(s)\n`);
|
|
129
|
-
}
|
|
130
|
-
if (args[0] === "context") {
|
|
131
|
-
const usesDefaultConnection = !!env.DBX_CONNECTION && args.length === 1;
|
|
132
|
-
ensureArgCount(args, usesDefaultConnection ? 1 : 2, "dbx context");
|
|
133
|
-
const connectionName = usesDefaultConnection ? env.DBX_CONNECTION : required(args[1], "Connection name is required.");
|
|
134
|
-
const config = await findConnectionOrThrow(backend, connectionName);
|
|
135
|
-
const context = await buildSchemaContext(backend, config, {
|
|
136
|
-
schema: flags.schema,
|
|
137
|
-
tables: flags.tables,
|
|
138
|
-
maxTables: flags.maxTables,
|
|
139
|
-
});
|
|
140
|
-
if (flags.format === "json")
|
|
141
|
-
return okJson(context);
|
|
142
|
-
if (flags.format === "csv")
|
|
143
|
-
throw new CliError("INVALID_OPTION", "CSV format is not supported for dbx context.");
|
|
144
|
-
return ok(`${formatSchemaContext(context)}\n`);
|
|
145
|
-
}
|
|
146
|
-
if (args[0] === "open") {
|
|
147
|
-
ensureArgCount(args, 3, "dbx open");
|
|
148
|
-
const connectionName = required(args[1], "Connection name is required.");
|
|
149
|
-
const table = required(args[2], "Table name is required.");
|
|
150
|
-
const response = await postBridge("/open-table", {
|
|
151
|
-
connection_name: connectionName,
|
|
152
|
-
table,
|
|
153
|
-
schema: flags.schema,
|
|
154
|
-
database: flags.database,
|
|
155
|
-
});
|
|
156
|
-
if (!response.ok) {
|
|
157
|
-
return fail("DBX_NOT_RUNNING", response.text || "DBX is not running. Please start DBX first.", flags.json);
|
|
158
|
-
}
|
|
159
|
-
if (flags.format === "json")
|
|
160
|
-
return okJson({ opened: true, connection: connectionName, table, schema: flags.schema, database: flags.database });
|
|
161
|
-
if (flags.format === "csv")
|
|
162
|
-
throw new CliError("INVALID_OPTION", "CSV format is not supported for dbx open.");
|
|
163
|
-
return ok(`Opened ${table} in DBX\n`);
|
|
164
|
-
}
|
|
165
|
-
return fail("USAGE", usage(), flags.json);
|
|
166
|
-
}
|
|
167
|
-
catch (error) {
|
|
168
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
169
|
-
const code = error instanceof CliError ? error.code : typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "ERROR";
|
|
170
|
-
const wantsJson = argv.includes("--json");
|
|
171
|
-
return fail(code, message, wantsJson);
|
|
172
|
-
}
|
|
173
|
-
finally {
|
|
174
|
-
await ownedBackend?.close?.().catch(() => { });
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
function parseFlags(argv) {
|
|
178
|
-
const args = [];
|
|
179
|
-
const flags = {
|
|
180
|
-
args,
|
|
181
|
-
json: false,
|
|
182
|
-
format: "table",
|
|
183
|
-
allowWrites: false,
|
|
184
|
-
allowDangerous: false,
|
|
185
|
-
help: false,
|
|
186
|
-
version: false,
|
|
187
|
-
};
|
|
188
|
-
for (let i = 0; i < argv.length; i++) {
|
|
189
|
-
const arg = argv[i];
|
|
190
|
-
if (arg === "--") {
|
|
191
|
-
args.push(...argv.slice(i + 1));
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
if (arg === "--json") {
|
|
195
|
-
flags.json = true;
|
|
196
|
-
flags.format = "json";
|
|
197
|
-
}
|
|
198
|
-
else if (arg === "--format")
|
|
199
|
-
flags.format = parseFormat(readOptionValue(argv, ++i, "--format"));
|
|
200
|
-
else if (arg === "--help" || arg === "-h")
|
|
201
|
-
flags.help = true;
|
|
202
|
-
else if (arg === "--version" || arg === "-V")
|
|
203
|
-
flags.version = true;
|
|
204
|
-
else if (arg === "--schema")
|
|
205
|
-
flags.schema = readOptionValue(argv, ++i, "--schema");
|
|
206
|
-
else if (arg === "--database")
|
|
207
|
-
flags.database = readOptionValue(argv, ++i, "--database");
|
|
208
|
-
else if (arg === "--tables")
|
|
209
|
-
flags.tables = splitCsv(readOptionValue(argv, ++i, "--tables"));
|
|
210
|
-
else if (arg === "--max-tables")
|
|
211
|
-
flags.maxTables = parsePositiveInt(readOptionValue(argv, ++i, "--max-tables"), "--max-tables");
|
|
212
|
-
else if (arg === "--limit")
|
|
213
|
-
flags.maxRows = parsePositiveInt(readOptionValue(argv, ++i, "--limit"), "--limit");
|
|
214
|
-
else if (arg === "--timeout")
|
|
215
|
-
flags.timeoutMs = parseDurationMs(readOptionValue(argv, ++i, "--timeout"), "--timeout");
|
|
216
|
-
else if (arg === "--file")
|
|
217
|
-
flags.file = readOptionValue(argv, ++i, "--file");
|
|
218
|
-
else if (arg === "--allow-writes")
|
|
219
|
-
flags.allowWrites = true;
|
|
220
|
-
else if (arg === "--allow-dangerous-sql")
|
|
221
|
-
flags.allowDangerous = true;
|
|
222
|
-
else if (arg.startsWith("-"))
|
|
223
|
-
throw new CliError("UNKNOWN_OPTION", `Unknown option: ${arg}`);
|
|
224
|
-
else
|
|
225
|
-
args.push(arg);
|
|
226
|
-
}
|
|
227
|
-
return flags;
|
|
228
|
-
}
|
|
229
|
-
function parseFormat(value) {
|
|
230
|
-
if (value === "table" || value === "json" || value === "csv")
|
|
231
|
-
return value;
|
|
232
|
-
throw new CliError("INVALID_OPTION", "--format must be one of: table, json, csv.");
|
|
233
|
-
}
|
|
234
|
-
function ensureArgCount(args, count, command) {
|
|
235
|
-
if (args.length !== count) {
|
|
236
|
-
throw new CliError("INVALID_ARGUMENT", `${command} expects ${count - 1} argument(s); received ${args.length - 1}.`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
function readOptionValue(argv, index, option) {
|
|
240
|
-
const value = argv[index];
|
|
241
|
-
if (!value || value.startsWith("-")) {
|
|
242
|
-
throw new CliError("INVALID_OPTION", `${option} requires a value.`);
|
|
243
|
-
}
|
|
244
|
-
return value;
|
|
245
|
-
}
|
|
246
|
-
function parsePositiveInt(value, option) {
|
|
247
|
-
const parsed = Number(value);
|
|
248
|
-
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
249
|
-
throw new CliError("INVALID_OPTION", `${option} must be a positive integer.`);
|
|
250
|
-
}
|
|
251
|
-
return parsed;
|
|
252
|
-
}
|
|
253
|
-
function parseDurationMs(value, option) {
|
|
254
|
-
const match = value.match(/^(\d+)(ms|s|m)?$/);
|
|
255
|
-
if (!match) {
|
|
256
|
-
throw new CliError("INVALID_OPTION", `${option} must be a positive duration such as 500ms, 10s, or 1m.`);
|
|
257
|
-
}
|
|
258
|
-
const amount = Number(match[1]);
|
|
259
|
-
if (!Number.isInteger(amount) || amount < 1) {
|
|
260
|
-
throw new CliError("INVALID_OPTION", `${option} must be a positive duration such as 500ms, 10s, or 1m.`);
|
|
261
|
-
}
|
|
262
|
-
const unit = match[2] ?? "ms";
|
|
263
|
-
if (unit === "ms")
|
|
264
|
-
return amount;
|
|
265
|
-
if (unit === "s")
|
|
266
|
-
return amount * 1000;
|
|
267
|
-
return amount * 60_000;
|
|
268
|
-
}
|
|
269
|
-
function parseBooleanEnv(value) {
|
|
270
|
-
if (value === undefined)
|
|
271
|
-
return false;
|
|
272
|
-
const normalized = value.trim().toLowerCase();
|
|
273
|
-
return normalized === "1" || normalized === "true";
|
|
274
|
-
}
|
|
275
|
-
function sqlSafetyFromCliEnv(env) {
|
|
276
|
-
return {
|
|
277
|
-
allowWrites: parseBooleanEnv(env.DBX_MCP_ALLOW_WRITES),
|
|
278
|
-
allowDangerous: parseBooleanEnv(env.DBX_MCP_ALLOW_DANGEROUS_SQL),
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
function splitCsv(value) {
|
|
282
|
-
return (value ?? "")
|
|
283
|
-
.split(",")
|
|
284
|
-
.map((part) => part.trim())
|
|
285
|
-
.filter(Boolean);
|
|
286
|
-
}
|
|
287
|
-
async function findConnectionOrThrow(backend, name) {
|
|
288
|
-
const config = await backend.findConnection(name);
|
|
289
|
-
if (!config)
|
|
290
|
-
throw new CliError("CONNECTION_NOT_FOUND", `Connection "${name}" not found.`);
|
|
291
|
-
return config;
|
|
292
|
-
}
|
|
293
|
-
function required(value, message) {
|
|
294
|
-
if (!value)
|
|
295
|
-
throw new Error(message);
|
|
296
|
-
return value;
|
|
297
|
-
}
|
|
298
|
-
function ok(stdout) {
|
|
299
|
-
return { exitCode: 0, stdout, stderr: "" };
|
|
300
|
-
}
|
|
301
|
-
function okJson(payload) {
|
|
302
|
-
return ok(`${JSON.stringify(payload, null, 2)}\n`);
|
|
303
|
-
}
|
|
304
|
-
function fail(code, message, json) {
|
|
305
|
-
const text = json ? `${JSON.stringify(errorPayload(code, message), null, 2)}\n` : `${formatErrorMessage(code, message)}\n`;
|
|
306
|
-
return { exitCode: 1, stdout: "", stderr: text };
|
|
307
|
-
}
|
|
308
|
-
function usage() {
|
|
309
|
-
return [
|
|
310
|
-
"Usage:",
|
|
311
|
-
" dbx doctor [--json]",
|
|
312
|
-
" dbx capabilities [--json]",
|
|
313
|
-
" dbx connections list [--json]",
|
|
314
|
-
" dbx schema list <connection> [--schema name] [--json]",
|
|
315
|
-
" dbx schema describe <connection> <table> [--schema name] [--json]",
|
|
316
|
-
" dbx query <connection> <sql> [--file path] [--limit n] [--timeout 10s] [--allow-writes] [--allow-dangerous-sql] [--json]",
|
|
317
|
-
" dbx context <connection> [--schema name] [--tables a,b] [--max-tables n] [--json]",
|
|
318
|
-
" dbx open <connection> <table> [--schema name] [--database name] [--json]",
|
|
319
|
-
].join("\n");
|
|
320
|
-
}
|
|
321
|
-
function formatDoctor(diagnostics) {
|
|
322
|
-
const rows = [
|
|
323
|
-
["App data directory", diagnostics.appDataDir],
|
|
324
|
-
["DBX database", diagnostics.dbPathExists ? `found (${diagnostics.dbPath})` : `missing (${diagnostics.dbPath})`],
|
|
325
|
-
["Connections table", diagnostics.connectionsTableExists ? `${diagnostics.connectionRowCount} row(s)` : "missing"],
|
|
326
|
-
["Connection loading", diagnostics.loadConnectionsOk ? `ok (${diagnostics.loadedConnectionCount} loaded)` : `failed (${diagnostics.loadConnectionsError ?? "unknown error"})`],
|
|
327
|
-
...(diagnostics.loadConnectionsHint ? [["Connection fix", diagnostics.loadConnectionsHint]] : []),
|
|
328
|
-
["Desktop bridge", diagnostics.bridgePortFileExists ? `available (${diagnostics.bridgeUrl ?? diagnostics.bridgePortFile})` : "not running"],
|
|
329
|
-
["Direct query types", diagnostics.directQueryTypes.join(", ")],
|
|
330
|
-
["Bridge-required types", diagnostics.bridgeRequiredTypes.join(", ")],
|
|
331
|
-
];
|
|
332
|
-
return `${mdTable(["Check", "Value"], rows)}\n`;
|
|
333
|
-
}
|
|
334
|
-
async function packageVersion() {
|
|
335
|
-
const packageJson = await readFile(new URL("../package.json", import.meta.url), "utf-8");
|
|
336
|
-
const parsed = JSON.parse(packageJson);
|
|
337
|
-
return parsed.version ?? "0.0.0";
|
|
338
|
-
}
|
|
339
|
-
async function main() {
|
|
340
|
-
const result = await runCli(process.argv.slice(2));
|
|
341
|
-
if (result.stdout)
|
|
342
|
-
process.stdout.write(result.stdout);
|
|
343
|
-
if (result.stderr)
|
|
344
|
-
process.stderr.write(result.stderr);
|
|
345
|
-
process.exitCode = result.exitCode;
|
|
346
|
-
}
|
|
347
|
-
if (isMainModule(import.meta.url, process.argv[1])) {
|
|
348
|
-
main().catch((error) => {
|
|
349
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
350
|
-
process.exitCode = 1;
|
|
351
|
-
});
|
|
352
|
-
}
|