@mohxmd/dbstudio 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +275 -0
- package/package.json +4 -8
- package/bin/dbstudio +0 -0
- package/lib/commands/studio.ts +0 -98
- package/lib/commands/tunnel.ts +0 -60
- package/lib/constants/help.ts +0 -33
- package/lib/utils/args.ts +0 -50
- package/lib/utils/config.ts +0 -70
- package/lib/utils/dialect.ts +0 -20
- package/main.ts +0 -41
package/dist/index.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// lib/constants/help.ts
|
|
5
|
+
var HELP = `
|
|
6
|
+
dbstudio \u2014 spin up Drizzle Studio from any database URL
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
dbstudio <database-url> [options]
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
dbstudio postgresql://user:pass@localhost:5432/mydb
|
|
13
|
+
dbstudio mysql://user:pass@localhost:3306/mydb
|
|
14
|
+
dbstudio sqlite:./local.db
|
|
15
|
+
|
|
16
|
+
# Quick share (no CF account needed, temporary URL)
|
|
17
|
+
dbstudio postgresql://... --share
|
|
18
|
+
|
|
19
|
+
# Named tunnel (persistent URL, needs CF account + domain)
|
|
20
|
+
dbstudio postgresql://... --share --tunnel dbstudio --hostname db.yourteam.com
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--port <number> Port to run studio on (default: 4983)
|
|
24
|
+
--host <string> Host to bind to (default: 127.0.0.1, or 0.0.0.0 with --share)
|
|
25
|
+
--open Auto-open in browser
|
|
26
|
+
--share Expose via Cloudflare Tunnel
|
|
27
|
+
--tunnel <name> Named CF tunnel (requires --hostname)
|
|
28
|
+
--hostname <domain> Public hostname for named tunnel
|
|
29
|
+
-h, --help Show this help
|
|
30
|
+
-v, --version Show version
|
|
31
|
+
|
|
32
|
+
Tunnel setup (one time):
|
|
33
|
+
yay -S cloudflared
|
|
34
|
+
cloudflared tunnel login
|
|
35
|
+
cloudflared tunnel create <name>
|
|
36
|
+
# add CNAME in Cloudflare dashboard \u2192 <tunnel-id>.cfargotunnel.com
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
// lib/utils/args.ts
|
|
40
|
+
function getFlag(args, flag) {
|
|
41
|
+
const idx = args.indexOf(flag);
|
|
42
|
+
if (idx === -1)
|
|
43
|
+
return null;
|
|
44
|
+
const val = args[idx + 1];
|
|
45
|
+
return val && !val.startsWith("--") ? val : null;
|
|
46
|
+
}
|
|
47
|
+
function parseStudioOptions(args) {
|
|
48
|
+
const dbUrl = args[0];
|
|
49
|
+
if (!dbUrl || dbUrl.startsWith("--")) {
|
|
50
|
+
throw new Error("Please provide a database URL as the first argument.");
|
|
51
|
+
}
|
|
52
|
+
const shouldShare = args.includes("--share");
|
|
53
|
+
const tunnelName = getFlag(args, "--tunnel");
|
|
54
|
+
const publicHostname = getFlag(args, "--hostname");
|
|
55
|
+
if (tunnelName && !publicHostname) {
|
|
56
|
+
throw new Error(`--tunnel requires --hostname <your-domain>
|
|
57
|
+
Example: --tunnel dbstudio --hostname db.yourteam.com`);
|
|
58
|
+
}
|
|
59
|
+
const explicitHost = getFlag(args, "--host");
|
|
60
|
+
const host = explicitHost ?? (shouldShare ? "0.0.0.0" : "127.0.0.1");
|
|
61
|
+
return {
|
|
62
|
+
dbUrl,
|
|
63
|
+
port: getFlag(args, "--port") ?? "4983",
|
|
64
|
+
host,
|
|
65
|
+
shouldOpen: args.includes("--open"),
|
|
66
|
+
shouldShare,
|
|
67
|
+
tunnelName,
|
|
68
|
+
publicHostname
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// lib/utils/dialect.ts
|
|
73
|
+
function detectDialect(url) {
|
|
74
|
+
if (url.startsWith("postgresql://") || url.startsWith("postgres://"))
|
|
75
|
+
return "postgresql";
|
|
76
|
+
if (url.startsWith("mysql://") || url.startsWith("mysql2://"))
|
|
77
|
+
return "mysql";
|
|
78
|
+
if (url.startsWith("sqlite:") || url.startsWith("file:") || url === ":memory:" || url.endsWith(".db") || url.endsWith(".sqlite"))
|
|
79
|
+
return "sqlite";
|
|
80
|
+
throw new Error(`Could not detect dialect from URL: ${url}
|
|
81
|
+
Supported: postgresql://, mysql://, sqlite:, file:`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// lib/utils/config.ts
|
|
85
|
+
import { writeFileSync, unlinkSync, existsSync } from "fs";
|
|
86
|
+
import { tmpdir, homedir } from "os";
|
|
87
|
+
import { join } from "path";
|
|
88
|
+
var tempFiles = [];
|
|
89
|
+
function cleanupAll() {
|
|
90
|
+
for (const f of tempFiles) {
|
|
91
|
+
if (existsSync(f))
|
|
92
|
+
unlinkSync(f);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function writeTempFile(name, content) {
|
|
96
|
+
const path = join(tmpdir(), `${name}-${Date.now()}`);
|
|
97
|
+
writeFileSync(path, content, "utf8");
|
|
98
|
+
tempFiles.push(path);
|
|
99
|
+
return path;
|
|
100
|
+
}
|
|
101
|
+
function generateDrizzleConfig(url, dialect) {
|
|
102
|
+
if (dialect === "sqlite") {
|
|
103
|
+
const filePath = url.replace(/^sqlite:\/\//, "").replace(/^sqlite:/, "").replace(/^file:\/\//, "").replace(/^file:/, "");
|
|
104
|
+
return `export default {
|
|
105
|
+
dialect: "sqlite",
|
|
106
|
+
dbCredentials: { url: "${filePath}" },
|
|
107
|
+
};`;
|
|
108
|
+
}
|
|
109
|
+
return `export default {
|
|
110
|
+
dialect: "${dialect}",
|
|
111
|
+
dbCredentials: { url: "${url}" },
|
|
112
|
+
};`;
|
|
113
|
+
}
|
|
114
|
+
function createDrizzleConfig(url, dialect) {
|
|
115
|
+
return writeTempFile("dbstudio.config.ts", generateDrizzleConfig(url, dialect));
|
|
116
|
+
}
|
|
117
|
+
function generateTunnelConfig(tunnelName, hostname, port) {
|
|
118
|
+
const credPath = join(homedir(), ".cloudflared");
|
|
119
|
+
return `tunnel: ${tunnelName}
|
|
120
|
+
credentials-file: ${credPath}/${tunnelName}.json
|
|
121
|
+
|
|
122
|
+
ingress:
|
|
123
|
+
- hostname: ${hostname}
|
|
124
|
+
service: http://localhost:${port}
|
|
125
|
+
- service: http_status:404
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
function createTunnelConfig(tunnelName, hostname, port) {
|
|
129
|
+
return writeTempFile("dbstudio-tunnel.yml", generateTunnelConfig(tunnelName, hostname, port));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// lib/commands/tunnel.ts
|
|
133
|
+
import { spawn } from "child_process";
|
|
134
|
+
function launchTunnel(options) {
|
|
135
|
+
const { port, tunnelName, publicHostname, shouldOpen } = options;
|
|
136
|
+
let tunnelArgs;
|
|
137
|
+
if (tunnelName && publicHostname) {
|
|
138
|
+
const tunnelConfigPath = createTunnelConfig(tunnelName, publicHostname, port);
|
|
139
|
+
tunnelArgs = ["tunnel", "--config", tunnelConfigPath, "run", tunnelName];
|
|
140
|
+
} else {
|
|
141
|
+
tunnelArgs = ["tunnel", "--url", `http://localhost:${port}`];
|
|
142
|
+
}
|
|
143
|
+
const tunnel = spawn("cloudflared", tunnelArgs, {
|
|
144
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
145
|
+
shell: false
|
|
146
|
+
});
|
|
147
|
+
tunnel.stderr?.on("data", (data) => {
|
|
148
|
+
const line = data.toString();
|
|
149
|
+
process.stderr.write(data);
|
|
150
|
+
if (!tunnelName) {
|
|
151
|
+
const match = line.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
152
|
+
if (match) {
|
|
153
|
+
console.log(`
|
|
154
|
+
\u2705 Public URL: ${match[0]}`);
|
|
155
|
+
console.log(` Share this link with your team
|
|
156
|
+
`);
|
|
157
|
+
if (shouldOpen) {
|
|
158
|
+
spawn("xdg-open", [match[0]], { stdio: "ignore" });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
tunnel.on("error", (err) => {
|
|
164
|
+
console.error(`
|
|
165
|
+
\u274C Failed to start cloudflared: ${err.message}`);
|
|
166
|
+
console.error(` Install it with: yay -S cloudflared`);
|
|
167
|
+
});
|
|
168
|
+
tunnel.on("exit", (code) => {
|
|
169
|
+
if (code !== 0)
|
|
170
|
+
console.error(`
|
|
171
|
+
\u26A0\uFE0F Tunnel exited with code ${code}`);
|
|
172
|
+
});
|
|
173
|
+
return tunnel;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// lib/commands/studio.ts
|
|
177
|
+
import { spawn as spawn2 } from "child_process";
|
|
178
|
+
var procs = [];
|
|
179
|
+
function killAll() {
|
|
180
|
+
for (const p of procs) {
|
|
181
|
+
if (!p.killed)
|
|
182
|
+
p.kill("SIGTERM");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function registerCleanup() {
|
|
186
|
+
process.on("exit", cleanupAll);
|
|
187
|
+
process.on("SIGINT", () => {
|
|
188
|
+
console.log(`
|
|
189
|
+
|
|
190
|
+
\uD83D\uDC4B Shutting down dbstudio...`);
|
|
191
|
+
killAll();
|
|
192
|
+
cleanupAll();
|
|
193
|
+
process.exit(0);
|
|
194
|
+
});
|
|
195
|
+
process.on("SIGTERM", () => {
|
|
196
|
+
killAll();
|
|
197
|
+
cleanupAll();
|
|
198
|
+
process.exit(0);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async function runStudioCommand(options) {
|
|
202
|
+
const { dbUrl, port, host, shouldOpen, shouldShare, tunnelName, publicHostname } = options;
|
|
203
|
+
const dialect = detectDialect(dbUrl);
|
|
204
|
+
const drizzleConfig = createDrizzleConfig(dbUrl, dialect);
|
|
205
|
+
const safeUrl = dbUrl.replace(/:\/\/.*@/, "://<credentials>@");
|
|
206
|
+
registerCleanup();
|
|
207
|
+
console.log(`
|
|
208
|
+
\uD83D\uDE80 dbstudio`);
|
|
209
|
+
console.log(` Dialect : ${dialect}`);
|
|
210
|
+
console.log(` URL : ${safeUrl}`);
|
|
211
|
+
console.log(` Studio : https://local.drizzle.studio`);
|
|
212
|
+
if (shouldShare && !tunnelName) {
|
|
213
|
+
console.log(` Tunnel : starting... public URL coming shortly`);
|
|
214
|
+
}
|
|
215
|
+
if (tunnelName && publicHostname) {
|
|
216
|
+
console.log(` Tunnel : https://${publicHostname}`);
|
|
217
|
+
}
|
|
218
|
+
console.log();
|
|
219
|
+
const studioArgs = [
|
|
220
|
+
"drizzle-kit",
|
|
221
|
+
"studio",
|
|
222
|
+
`--config=${drizzleConfig}`,
|
|
223
|
+
`--port=${port}`,
|
|
224
|
+
`--host=${host}`
|
|
225
|
+
];
|
|
226
|
+
if (shouldOpen && !shouldShare)
|
|
227
|
+
studioArgs.push("--open");
|
|
228
|
+
const studio = Bun.spawn(["bunx", ...studioArgs], {
|
|
229
|
+
stdin: "ignore",
|
|
230
|
+
stdout: "inherit",
|
|
231
|
+
stderr: "inherit"
|
|
232
|
+
});
|
|
233
|
+
if (shouldShare) {
|
|
234
|
+
await Bun.sleep(2000);
|
|
235
|
+
const tunnel = launchTunnel({ port, tunnelName, publicHostname, shouldOpen });
|
|
236
|
+
procs.push(tunnel);
|
|
237
|
+
if (shouldOpen && tunnelName && publicHostname) {
|
|
238
|
+
spawn2("xdg-open", [`https://${publicHostname}`], { stdio: "ignore" });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const exitCode = await studio.exited;
|
|
242
|
+
killAll();
|
|
243
|
+
cleanupAll();
|
|
244
|
+
return exitCode ?? 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// main.ts
|
|
248
|
+
var args = process.argv.slice(2);
|
|
249
|
+
if (args.length === 0 || args.includes("-h") || args.includes("--help")) {
|
|
250
|
+
console.log(HELP);
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
253
|
+
if (args.includes("-v") || args.includes("--version")) {
|
|
254
|
+
const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
|
|
255
|
+
console.log(`dbstudio v${pkg.version}`);
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const options = parseStudioOptions(args);
|
|
260
|
+
const code = await runStudioCommand(options);
|
|
261
|
+
process.exit(code);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
264
|
+
if (msg.startsWith("Please provide")) {
|
|
265
|
+
console.error(`\u274C ${msg}`);
|
|
266
|
+
console.error(" Run dbstudio --help for usage.");
|
|
267
|
+
} else if (msg.startsWith("Could not detect")) {
|
|
268
|
+
console.error(`\u274C ${msg}`);
|
|
269
|
+
} else if (msg.startsWith("--tunnel requires")) {
|
|
270
|
+
console.error(`\u274C ${msg}`);
|
|
271
|
+
} else {
|
|
272
|
+
console.error(`\u274C Unexpected error: ${msg}`);
|
|
273
|
+
}
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mohxmd/dbstudio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Spin up Drizzle Studio from any database URL",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"module": "main.ts",
|
|
8
7
|
"bin": {
|
|
9
|
-
"dbstudio": "./
|
|
8
|
+
"dbstudio": "./dist/index.js"
|
|
10
9
|
},
|
|
11
10
|
"scripts": {
|
|
12
11
|
"start": "bun main.ts",
|
|
13
12
|
"dev": "bun --watch main.ts",
|
|
13
|
+
"build": "bun build main.ts --outfile dist/index.js --target bun",
|
|
14
14
|
"compile": "bun build main.ts --compile --outfile bin/dbstudio",
|
|
15
15
|
"compile:linux": "bun build main.ts --compile --target=bun-linux-x64 --outfile bin/dbstudio-linux",
|
|
16
16
|
"compile:mac": "bun build main.ts --compile --target=bun-darwin-arm64 --outfile bin/dbstudio-mac"
|
|
@@ -19,8 +19,7 @@
|
|
|
19
19
|
"bun": ">=1.0.0"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
|
-
"
|
|
23
|
-
"lib",
|
|
22
|
+
"dist",
|
|
24
23
|
"README.md",
|
|
25
24
|
"LICENSE"
|
|
26
25
|
],
|
|
@@ -30,8 +29,5 @@
|
|
|
30
29
|
},
|
|
31
30
|
"devDependencies": {
|
|
32
31
|
"@types/bun": "latest"
|
|
33
|
-
},
|
|
34
|
-
"peerDependencies": {
|
|
35
|
-
"typescript": "latest"
|
|
36
32
|
}
|
|
37
33
|
}
|
package/bin/dbstudio
DELETED
|
Binary file
|
package/lib/commands/studio.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
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";
|
package/lib/commands/tunnel.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
}
|
package/lib/constants/help.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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
|
-
`;
|
package/lib/utils/args.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
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
|
-
}
|
package/lib/utils/config.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
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
|
-
}
|
package/lib/utils/dialect.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
}
|