db.ts101 lines · main
1import { spawnSync } from 'node:child_process';
2
3import { apiCall, ApiCallError } from '../api-client.js';
4import { readCredentials } from '../config.js';
5import { readProjectConfig } from '../project-config.js';
6import { banner, blankLine, error as printError, step, success } from '../output.js';
7
8interface ShellTokenResponse {
9 dsn: string;
10 role: string;
11 expiresAt: string;
12}
13
14export async function runDb(argv: readonly string[]): Promise<number> {
15 const [sub] = argv;
16 if (!sub || sub === '--help' || sub === '-h') {
17 banner('db');
18 blankLine();
19 step('briven db shell open psql against your project database');
20 return sub ? 0 : 1;
21 }
22 if (sub !== 'shell') {
23 printError(`unknown db subcommand: ${sub}`);
24 return 1;
25 }
26 return runShell();
27}
28
29async function runShell(): Promise<number> {
30 const local = await readProjectConfig();
31 if (!local) {
32 printError('no briven.json in this directory.');
33 step('run: briven init');
34 return 1;
35 }
36 if (!local.projectId) {
37 printError('briven.json has no projectId — link this directory first.');
38 step('run: briven link');
39 return 1;
40 }
41 const creds = await readCredentials();
42 const cred = creds.projects[local.projectId];
43 if (!cred) {
44 printError(`no stored credentials for ${local.projectId}.`);
45 step('run: briven login --project <id> --key <brk_...>');
46 return 1;
47 }
48
49 banner('db shell');
50 step(`project ${local.projectId}`);
51
52 let token: ShellTokenResponse;
53 try {
54 token = await apiCall<ShellTokenResponse>(`/v1/projects/${local.projectId}/db/shell-token`, {
55 method: 'POST',
56 apiOrigin: cred.apiOrigin,
57 apiKey: cred.apiKey,
58 });
59 } catch (err) {
60 if (err instanceof ApiCallError) {
61 printError(`server rejected: ${err.code} (${err.status})`);
62 } else {
63 printError(err instanceof Error ? err.message : 'unknown error');
64 }
65 return 1;
66 }
67
68 step(`role ${token.role}`);
69 step(`expires ${formatExpiry(token.expiresAt)}`);
70 blankLine();
71
72 // The data plane is Postgres-wire DoltGres, so we hand the short-lived
73 // DSN to psql (libpq accepts a postgres:// connection string positionally).
74 const result = spawnSync('psql', [token.dsn], { stdio: 'inherit' });
75 if (result.error) {
76 const code = (result.error as NodeJS.ErrnoException).code;
77 if (code === 'ENOENT') {
78 printError('psql not found — install the PostgreSQL client tools (libpq).');
79 step('macOS: brew install libpq (then add its bin to PATH, or: brew install postgresql)');
80 step('Linux: sudo apt install postgresql-client (Debian/Ubuntu)');
81 return 1;
82 }
83 printError(result.error.message);
84 return 1;
85 }
86 if (result.status == null) {
87 // Signalled exit — treat as abnormal but don't print an error; psql
88 // already handed control back to the terminal.
89 return 1;
90 }
91 if (result.status === 0) {
92 success('session closed');
93 }
94 return result.status;
95}
96
97function formatExpiry(iso: string): string {
98 const then = new Date(iso);
99 const mins = Math.max(0, Math.round((then.getTime() - Date.now()) / 60_000));
100 return `${then.toISOString().replace('T', ' ').slice(0, 19)} (in ${mins}m)`;
101}