db.ts101 lines · main
| 1 | import { spawnSync } from 'node:child_process'; |
| 2 | |
| 3 | import { apiCall, ApiCallError } from '../api-client.js'; |
| 4 | import { readCredentials } from '../config.js'; |
| 5 | import { readProjectConfig } from '../project-config.js'; |
| 6 | import { banner, blankLine, error as printError, step, success } from '../output.js'; |
| 7 | |
| 8 | interface ShellTokenResponse { |
| 9 | dsn: string; |
| 10 | role: string; |
| 11 | expiresAt: string; |
| 12 | } |
| 13 | |
| 14 | export 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 | |
| 29 | async 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 | |
| 97 | function 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 | } |