@mohxmd/dbstudio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # dbstudio
2
+
3
+ > Spin up Drizzle Studio instantly from any database URL — no project setup needed.
4
+
5
+ ## Install
6
+
7
+ ### Global
8
+ ```bash
9
+ npm i -g dbstudio
10
+ # or
11
+ bun add -g dbstudio
12
+ ```
13
+
14
+ ### Without installing
15
+ ```bash
16
+ bunx dbstudio <url>
17
+ # or
18
+ bun x dbstudio <url>
19
+ ```
20
+
21
+ ### Standalone binaries
22
+ Download prebuilt binaries from GitHub Releases (Linux/macOS/Windows).
23
+
24
+ ## Usage
25
+
26
+ ```bash
27
+ # PostgreSQL
28
+ dbstudio postgresql://user:pass@localhost:5432/mydb
29
+
30
+ # MySQL
31
+ dbstudio mysql://user:pass@localhost:3306/mydb
32
+
33
+ # SQLite
34
+ dbstudio sqlite:./local.db
35
+
36
+ # Custom port
37
+ dbstudio postgresql://... --port 8080
38
+
39
+ # Auto-open browser
40
+ dbstudio postgresql://... --open
41
+
42
+ # Quick share with a temporary public URL
43
+ dbstudio postgresql://... --share
44
+
45
+ # Named tunnel with your hostname
46
+ dbstudio postgresql://... --share --tunnel dbstudio --hostname db.yourteam.com
47
+ ```
48
+
49
+ ## How it works
50
+
51
+ 1. Detects dialect from your URL (postgresql / mysql / sqlite)
52
+ 2. Writes a temporary `drizzle.config.ts` in your OS temp directory
53
+ 3. Spins up `drizzle-kit studio`
54
+ 4. Cleans up the temp file when the process exits (including Ctrl+C)
55
+
56
+ No project config directory. No project setup. Just pass a URL.
57
+
58
+ ## Options
59
+
60
+ | Flag | Default | Description |
61
+ |------|---------|-------------|
62
+ | `--port` | `4983` | Port to run studio on |
63
+ | `--host` | `127.0.0.1` | Host to bind to (`0.0.0.0` is used automatically with `--share` unless explicitly set) |
64
+ | `--open` | false | Auto-open in browser |
65
+ | `--share` | false | Expose Studio through Cloudflare Tunnel |
66
+ | `--tunnel` | | Named Cloudflare tunnel (requires `--hostname`) |
67
+ | `--hostname` | | Public hostname for named tunnel |
68
+ | `--help` | | Show help |
69
+ | `--version` | | Show version |
70
+
71
+ ## Requirements
72
+
73
+ - [Bun](https://bun.sh) >= 1.0
74
+ - `cloudflared` if you use `--share`
75
+
76
+ ## License
77
+
78
+ MIT
package/bin/dbstudio ADDED
Binary file
@@ -0,0 +1,98 @@
1
+ import type { StudioOptions } from "../utils/args";
2
+ import { detectDialect } from "../utils/dialect";
3
+ import { createDrizzleConfig, cleanupAll } from "../utils/config";
4
+ import { launchTunnel } from "./tunnel";
5
+ import type { ChildProcess } from "child_process";
6
+
7
+ const procs: ChildProcess[] = [];
8
+
9
+ function killAll() {
10
+ for (const p of procs) {
11
+ if (!p.killed) p.kill("SIGTERM");
12
+ }
13
+ }
14
+
15
+ function registerCleanup() {
16
+ process.on("exit", cleanupAll);
17
+ process.on("SIGINT", () => {
18
+ console.log("\n\nšŸ‘‹ Shutting down dbstudio...");
19
+ killAll();
20
+ cleanupAll();
21
+ process.exit(0);
22
+ });
23
+ process.on("SIGTERM", () => {
24
+ killAll();
25
+ cleanupAll();
26
+ process.exit(0);
27
+ });
28
+ }
29
+
30
+ export async function runStudioCommand(options: StudioOptions): Promise<number> {
31
+ const { dbUrl, port, host, shouldOpen, shouldShare, tunnelName, publicHostname } = options;
32
+
33
+ const dialect = detectDialect(dbUrl);
34
+ const drizzleConfig = createDrizzleConfig(dbUrl, dialect);
35
+ const safeUrl = dbUrl.replace(/:\/\/.*@/, "://<credentials>@");
36
+
37
+ registerCleanup();
38
+
39
+ // Print startup info
40
+
41
+ console.log(`\nšŸš€ dbstudio`);
42
+ console.log(` Dialect : ${dialect}`);
43
+ console.log(` URL : ${safeUrl}`);
44
+ console.log(` Studio : https://local.drizzle.studio`);
45
+
46
+ if (shouldShare && !tunnelName) {
47
+ console.log(` Tunnel : starting... public URL coming shortly`);
48
+ }
49
+ if (tunnelName && publicHostname) {
50
+ console.log(` Tunnel : https://${publicHostname}`);
51
+ }
52
+ console.log();
53
+
54
+ // Spawn drizzle-kit studio via bunx
55
+ // bunx is used so the binary works after `bun build --compile`
56
+ // without needing a local node_modules at runtime
57
+
58
+ const studioArgs = [
59
+ "drizzle-kit",
60
+ "studio",
61
+ `--config=${drizzleConfig}`,
62
+ `--port=${port}`,
63
+ `--host=${host}`,
64
+ ];
65
+
66
+ if (shouldOpen && !shouldShare) studioArgs.push("--open");
67
+
68
+ const studio = Bun.spawn(["bunx", ...studioArgs], {
69
+ stdin: "ignore",
70
+ stdout: "inherit",
71
+ stderr: "inherit",
72
+ });
73
+
74
+ // Launch tunnel after studio boots
75
+
76
+ if (shouldShare) {
77
+ // give drizzle-kit studio a moment to start before tunnel connects
78
+ await Bun.sleep(2000);
79
+
80
+ const tunnel = launchTunnel({ port, tunnelName, publicHostname, shouldOpen });
81
+ // cast needed since launchTunnel returns node ChildProcess
82
+ procs.push(tunnel as unknown as ChildProcess);
83
+
84
+ if (shouldOpen && tunnelName && publicHostname) {
85
+ spawn("xdg-open", [`https://${publicHostname}`], { stdio: "ignore" });
86
+ }
87
+ }
88
+
89
+ // Wait for studio to exit
90
+
91
+ const exitCode = await studio.exited;
92
+ killAll();
93
+ cleanupAll();
94
+ return exitCode ?? 0;
95
+ }
96
+
97
+ // node spawn needed for tunnel (pipe stdio support)
98
+ import { spawn } from "child_process";
@@ -0,0 +1,60 @@
1
+ import { spawn } from "child_process";
2
+ import { createTunnelConfig } from "../utils/config";
3
+
4
+ export function launchTunnel(options: {
5
+ port: string;
6
+ tunnelName: string | null;
7
+ publicHostname: string | null;
8
+ shouldOpen: boolean;
9
+ }) {
10
+ const { port, tunnelName, publicHostname, shouldOpen } = options;
11
+
12
+ let tunnelArgs: string[];
13
+
14
+ if (tunnelName && publicHostname) {
15
+ // named tunnel — generate temp config.yml, never touches ~/.cloudflared/config.yml
16
+ const tunnelConfigPath = createTunnelConfig(
17
+ tunnelName,
18
+ publicHostname,
19
+ port,
20
+ );
21
+ tunnelArgs = ["tunnel", "--config", tunnelConfigPath, "run", tunnelName];
22
+ } else {
23
+ // quick share — zero setup, temporary trycloudflare.com URL
24
+ tunnelArgs = ["tunnel", "--url", `http://localhost:${port}`];
25
+ }
26
+
27
+ const tunnel = spawn("cloudflared", tunnelArgs, {
28
+ stdio: ["ignore", "pipe", "pipe"],
29
+ shell: false,
30
+ });
31
+
32
+ // cloudflared prints the public URL to stderr
33
+ tunnel.stderr?.on("data", (data: Buffer) => {
34
+ const line = data.toString();
35
+ process.stderr.write(data);
36
+
37
+ // quick share: extract and highlight the URL when cloudflared prints it
38
+ if (!tunnelName) {
39
+ const match = line.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
40
+ if (match) {
41
+ console.log(`\nāœ… Public URL: ${match[0]}`);
42
+ console.log(` Share this link with your team\n`);
43
+ if (shouldOpen) {
44
+ spawn("xdg-open", [match[0]], { stdio: "ignore" });
45
+ }
46
+ }
47
+ }
48
+ });
49
+
50
+ tunnel.on("error", (err) => {
51
+ console.error(`\nāŒ Failed to start cloudflared: ${err.message}`);
52
+ console.error(` Install it with: yay -S cloudflared`);
53
+ });
54
+
55
+ tunnel.on("exit", (code) => {
56
+ if (code !== 0) console.error(`\nāš ļø Tunnel exited with code ${code}`);
57
+ });
58
+
59
+ return tunnel;
60
+ }
@@ -0,0 +1,33 @@
1
+ export const HELP = `
2
+ dbstudio — spin up Drizzle Studio from any database URL
3
+
4
+ Usage:
5
+ dbstudio <database-url> [options]
6
+
7
+ Examples:
8
+ dbstudio postgresql://user:pass@localhost:5432/mydb
9
+ dbstudio mysql://user:pass@localhost:3306/mydb
10
+ dbstudio sqlite:./local.db
11
+
12
+ # Quick share (no CF account needed, temporary URL)
13
+ dbstudio postgresql://... --share
14
+
15
+ # Named tunnel (persistent URL, needs CF account + domain)
16
+ dbstudio postgresql://... --share --tunnel dbstudio --hostname db.yourteam.com
17
+
18
+ Options:
19
+ --port <number> Port to run studio on (default: 4983)
20
+ --host <string> Host to bind to (default: 127.0.0.1, or 0.0.0.0 with --share)
21
+ --open Auto-open in browser
22
+ --share Expose via Cloudflare Tunnel
23
+ --tunnel <name> Named CF tunnel (requires --hostname)
24
+ --hostname <domain> Public hostname for named tunnel
25
+ -h, --help Show this help
26
+ -v, --version Show version
27
+
28
+ Tunnel setup (one time):
29
+ yay -S cloudflared
30
+ cloudflared tunnel login
31
+ cloudflared tunnel create <name>
32
+ # add CNAME in Cloudflare dashboard → <tunnel-id>.cfargotunnel.com
33
+ `;
@@ -0,0 +1,50 @@
1
+ export type StudioOptions = {
2
+ dbUrl: string;
3
+ port: string;
4
+ host: string;
5
+ shouldOpen: boolean;
6
+ shouldShare: boolean;
7
+ tunnelName: string | null;
8
+ publicHostname: string | null;
9
+ };
10
+
11
+ /** Read a flag value in the form `--flag <value>`. */
12
+ function getFlag(args: string[], flag: string): string | null {
13
+ const idx = args.indexOf(flag);
14
+ if (idx === -1) return null;
15
+ const val = args[idx + 1];
16
+ return val && !val.startsWith("--") ? val : null;
17
+ }
18
+
19
+ /** Parse and validate CLI arguments into studio runtime options. */
20
+ export function parseStudioOptions(args: string[]): StudioOptions {
21
+ const dbUrl = args[0];
22
+
23
+ if (!dbUrl || dbUrl.startsWith("--")) {
24
+ throw new Error("Please provide a database URL as the first argument.");
25
+ }
26
+
27
+ const shouldShare = args.includes("--share");
28
+ const tunnelName = getFlag(args, "--tunnel");
29
+ const publicHostname = getFlag(args, "--hostname");
30
+
31
+ if (tunnelName && !publicHostname) {
32
+ throw new Error(
33
+ "--tunnel requires --hostname <your-domain>\n Example: --tunnel dbstudio --hostname db.yourteam.com",
34
+ );
35
+ }
36
+
37
+ // when sharing, bind to all interfaces so cloudflared can reach studio
38
+ const explicitHost = getFlag(args, "--host");
39
+ const host = explicitHost ?? (shouldShare ? "0.0.0.0" : "127.0.0.1");
40
+
41
+ return {
42
+ dbUrl,
43
+ port: getFlag(args, "--port") ?? "4983",
44
+ host,
45
+ shouldOpen: args.includes("--open"),
46
+ shouldShare,
47
+ tunnelName,
48
+ publicHostname,
49
+ };
50
+ }
@@ -0,0 +1,70 @@
1
+ import { writeFileSync, unlinkSync, existsSync } from "fs";
2
+ import { tmpdir, homedir } from "os";
3
+ import { join } from "path";
4
+ import type { Dialect } from "./dialect";
5
+
6
+ // Temp file registry
7
+
8
+ const tempFiles: string[] = [];
9
+
10
+ /** Remove all temporary files created by this process. */
11
+ export function cleanupAll() {
12
+ for (const f of tempFiles) {
13
+ if (existsSync(f)) unlinkSync(f);
14
+ }
15
+ }
16
+
17
+ /** Write content to a uniquely named file in the OS temp directory. */
18
+ function writeTempFile(name: string, content: string): string {
19
+ const path = join(tmpdir(), `${name}-${Date.now()}`);
20
+ writeFileSync(path, content, "utf8");
21
+ tempFiles.push(path);
22
+ return path;
23
+ }
24
+
25
+ // Drizzle config
26
+
27
+ /** Build a minimal drizzle.config.ts string for the provided database URL. */
28
+ function generateDrizzleConfig(url: string, dialect: Dialect): string {
29
+ if (dialect === "sqlite") {
30
+ const filePath = url
31
+ .replace(/^sqlite:\/\//, "")
32
+ .replace(/^sqlite:/, "")
33
+ .replace(/^file:\/\//, "")
34
+ .replace(/^file:/, "");
35
+ return `export default {
36
+ dialect: "sqlite",
37
+ dbCredentials: { url: "${filePath}" },
38
+ };`;
39
+ }
40
+
41
+ return `export default {
42
+ dialect: "${dialect}",
43
+ dbCredentials: { url: "${url}" },
44
+ };`;
45
+ }
46
+
47
+ /** Create a temp drizzle config file and return its path. */
48
+ export function createDrizzleConfig(url: string, dialect: Dialect): string {
49
+ return writeTempFile("dbstudio.config.ts", generateDrizzleConfig(url, dialect));
50
+ }
51
+
52
+ // Cloudflare tunnel config
53
+
54
+ /** Build a temporary cloudflared config for a named tunnel run. */
55
+ function generateTunnelConfig(tunnelName: string, hostname: string, port: string): string {
56
+ const credPath = join(homedir(), ".cloudflared");
57
+ return `tunnel: ${tunnelName}
58
+ credentials-file: ${credPath}/${tunnelName}.json
59
+
60
+ ingress:
61
+ - hostname: ${hostname}
62
+ service: http://localhost:${port}
63
+ - service: http_status:404
64
+ `;
65
+ }
66
+
67
+ /** Create a temp cloudflared config file for a named tunnel and return its path. */
68
+ export function createTunnelConfig(tunnelName: string, hostname: string, port: string): string {
69
+ return writeTempFile("dbstudio-tunnel.yml", generateTunnelConfig(tunnelName, hostname, port));
70
+ }
@@ -0,0 +1,20 @@
1
+ export type Dialect = "postgresql" | "mysql" | "sqlite";
2
+
3
+ /** Detect Drizzle dialect from a database URL or SQLite-style path. */
4
+ export function detectDialect(url: string): Dialect {
5
+ if (url.startsWith("postgresql://") || url.startsWith("postgres://"))
6
+ return "postgresql";
7
+ if (url.startsWith("mysql://") || url.startsWith("mysql2://")) return "mysql";
8
+ if (
9
+ url.startsWith("sqlite:") ||
10
+ url.startsWith("file:") ||
11
+ url === ":memory:" ||
12
+ url.endsWith(".db") ||
13
+ url.endsWith(".sqlite")
14
+ )
15
+ return "sqlite";
16
+
17
+ throw new Error(
18
+ `Could not detect dialect from URL: ${url}\nSupported: postgresql://, mysql://, sqlite:, file:`,
19
+ );
20
+ }
package/main.ts ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { HELP } from "./lib/constants/help";
4
+ import { parseStudioOptions } from "./lib/utils/args";
5
+ import { runStudioCommand } from "./lib/commands/studio";
6
+
7
+ const args = process.argv.slice(2);
8
+
9
+ if (args.length === 0 || args.includes("-h") || args.includes("--help")) {
10
+ console.log(HELP);
11
+ process.exit(0);
12
+ }
13
+
14
+ if (args.includes("-v") || args.includes("--version")) {
15
+ const pkg = await Bun.file(
16
+ new URL("../package.json", import.meta.url),
17
+ ).json();
18
+ console.log(`dbstudio v${pkg.version}`);
19
+ process.exit(0);
20
+ }
21
+
22
+ try {
23
+ const options = parseStudioOptions(args);
24
+ const code = await runStudioCommand(options);
25
+ process.exit(code);
26
+ } catch (error) {
27
+ const msg = error instanceof Error ? error.message : String(error);
28
+
29
+ if (msg.startsWith("Please provide")) {
30
+ console.error(`āŒ ${msg}`);
31
+ console.error(" Run dbstudio --help for usage.");
32
+ } else if (msg.startsWith("Could not detect")) {
33
+ console.error(`āŒ ${msg}`);
34
+ } else if (msg.startsWith("--tunnel requires")) {
35
+ console.error(`āŒ ${msg}`);
36
+ } else {
37
+ console.error(`āŒ Unexpected error: ${msg}`);
38
+ }
39
+
40
+ process.exit(1);
41
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@mohxmd/dbstudio",
3
+ "version": "0.1.0",
4
+ "description": "Spin up Drizzle Studio from any database URL",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "module": "main.ts",
8
+ "bin": {
9
+ "dbstudio": "./bin/dbstudio"
10
+ },
11
+ "scripts": {
12
+ "start": "bun main.ts",
13
+ "dev": "bun --watch main.ts",
14
+ "compile": "bun build main.ts --compile --outfile bin/dbstudio",
15
+ "compile:linux": "bun build main.ts --compile --target=bun-linux-x64 --outfile bin/dbstudio-linux",
16
+ "compile:mac": "bun build main.ts --compile --target=bun-darwin-arm64 --outfile bin/dbstudio-mac"
17
+ },
18
+ "engines": {
19
+ "bun": ">=1.0.0"
20
+ },
21
+ "files": [
22
+ "main.ts",
23
+ "lib",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "dependencies": {
28
+ "drizzle-kit": "latest",
29
+ "pg": "latest"
30
+ },
31
+ "devDependencies": {
32
+ "@types/bun": "latest"
33
+ },
34
+ "peerDependencies": {
35
+ "typescript": "latest"
36
+ }
37
+ }