diff --git a/.changeset/link-cancel-nonzero-exit.md b/.changeset/link-cancel-nonzero-exit.md new file mode 100644 index 0000000..aeecb1f --- /dev/null +++ b/.changeset/link-cancel-nonzero-exit.md @@ -0,0 +1,5 @@ +--- +"@bunny.net/cli": patch +--- + +Cancelling an interactive selection prompt now exits non-zero so scripts and CI can tell a cancelled command apart from a successful one. Previously `db link`, `db regions update`, and `scripts env remove` printed a "Cancelled." line and exited `0` when you aborted the picker with Ctrl-C/Esc, making a no-op indistinguishable from success. They now exit `1` (and emit a proper `{"error":…}` payload under `--output json`), matching `scripts link`, `apps link`, and the shared `resolveDbId` selection prompt. Declining a confirmation ("Delete?", "Replace?") still exits `0` — that's a deliberate answer, not an abort. diff --git a/AGENTS.md b/AGENTS.md index 6ea5301..6d2356a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -233,19 +233,22 @@ bunny-cli/ │ │ │ └── delete.ts # Remove a profile │ │ ├── whoami.ts # Show authenticated account: name, email, profile (top-level: bunny whoami) │ │ ├── db/ -│ │ │ ├── index.ts # defineNamespace("db", ...) — registers all database commands -│ │ │ ├── constants.ts # Database status labels, region maps -│ │ │ ├── create.ts # Create a new database (interactive region selection or flags) -│ │ │ ├── delete.ts # Delete a database (double confirmation or --force) -│ │ │ ├── docs.ts # Open database documentation in browser -│ │ │ ├── link.ts # Link directory to a database (.bunny/database.json) -│ │ │ ├── list.ts # List all databases -│ │ │ ├── quickstart.ts # Generate quickstart guide for connecting to a database -│ │ │ ├── region-choices.ts # Shared: grouped region prompt choices by continent -│ │ │ ├── resolve-db.ts # Helper: resolve database ID from flag, manifest, .env, or interactive prompt -│ │ │ ├── shell.ts # Thin wrapper: credential resolution + delegates to @bunny.net/database-shell -│ │ │ ├── show.ts # Show database details (regions, size, status) -│ │ │ ├── usage.ts # Show database usage statistics +│ │ │ ├── index.ts # defineNamespace("db", ...) — registers all database commands +│ │ │ ├── constants.ts # Database status labels, region maps +│ │ │ ├── api.ts # Shared: typed v2 database/token API calls (fetchDatabase, fetchAllDatabases, generateToken, fetchLiveStatus, …) +│ │ │ ├── create.ts # Create a new database (interactive region selection or flags) +│ │ │ ├── delete.ts # Delete a database (double confirmation or --force) +│ │ │ ├── docs.ts # Open database documentation in browser +│ │ │ ├── link.ts # Link directory to a database (.bunny/database.json) +│ │ │ ├── list.ts # List all databases +│ │ │ ├── quickstart.ts # Generate quickstart guide for connecting to a database +│ │ │ ├── quickstart-snippets.ts # Shared: per-language connection code snippets (TypeScript, Go, Rust, .NET) +│ │ │ ├── region-choices.ts # Shared: grouped region prompt choices by continent +│ │ │ ├── resolve-db.ts # Helper: resolve database ID from flag, manifest, .env, or interactive prompt +│ │ │ ├── shell.ts # Thin wrapper: credential resolution + delegates to @bunny.net/database-shell +│ │ │ ├── show.ts # Show database details (regions, size, status) +│ │ │ ├── studio.ts # Open a visual database explorer in the browser (local web UI) +│ │ │ ├── usage.ts # Show database usage statistics │ │ │ ├── regions/ │ │ │ │ ├── index.ts # defineNamespace("regions", ...) — registers region commands │ │ │ │ ├── add.ts # Add primary/replica regions (interactive multiselect or flags) diff --git a/packages/cli/src/commands/db/api.ts b/packages/cli/src/commands/db/api.ts new file mode 100644 index 0000000..2460cb9 --- /dev/null +++ b/packages/cli/src/commands/db/api.ts @@ -0,0 +1,113 @@ +import type { createDbClient } from "@bunny.net/openapi-client"; +import type { components } from "@bunny.net/openapi-client/generated/database.d.ts"; +import { UserError } from "../../core/errors.ts"; +import { DB_PAGE_SIZE, TOKEN_TTL_MINUTES } from "./constants.ts"; + +type DbClient = ReturnType; +type Database = components["schemas"]["Database2"]; +type RegionConfig = components["schemas"]["ListConfigAPIResponse"]; +type GenerateTokenResponse = + components["schemas"]["GenerateTokenDatabaseV2Response"]; +type TokenAuthorization = + components["schemas"]["GenerateTokenDatabaseV2Payload"]["authorization"]; +type DBLiveStatus = components["schemas"]["DBLiveStatus"]; + +/** Fetch a single database by ID, throwing a UserError if it doesn't exist. */ +export async function fetchDatabase( + client: DbClient, + id: string, +): Promise { + const { data } = await client.GET("/v2/databases/{db_id}", { + params: { path: { db_id: id } }, + }); + if (!data?.db) throw new UserError(`Database ${id} not found.`); + return data.db; +} + +/** Fetch every database in the account, paginating until exhausted. */ +export async function fetchAllDatabases(client: DbClient): Promise { + const all: Database[] = []; + let page = 1; + + while (true) { + const { data } = await client.GET("/v2/databases", { + params: { query: { page, per_page: DB_PAGE_SIZE } }, + }); + + all.push(...(data?.databases ?? [])); + + if (!data?.page_info?.has_more_items) break; + page++; + } + + return all; +} + +/** Fetch the global region configuration, throwing if unavailable. */ +export async function fetchRegionConfig( + client: DbClient, +): Promise { + const { data } = await client.GET("/v1/config", { params: {} }); + if (!data) throw new UserError("Could not fetch region configuration."); + return data; +} + +/** Fetch a database and the region config in parallel. */ +export async function fetchDatabaseWithRegions( + client: DbClient, + id: string, +): Promise<{ db: Database; config: RegionConfig }> { + const [db, config] = await Promise.all([ + fetchDatabase(client, id), + fetchRegionConfig(client), + ]); + return { db, config }; +} + +/** Build a region id → display name lookup from the region config. */ +export function regionNameMap(config: RegionConfig): Map { + const map = new Map(); + for (const r of [...config.primary_regions, ...config.replica_regions]) { + map.set(r.id, r.name); + } + return map; +} + +/** Generate an auth token for a database. */ +export async function generateToken( + client: DbClient, + id: string, + opts: { authorization: TokenAuthorization; expiresAt: string | null }, +): Promise { + const { data } = await client.PUT("/v2/databases/{db_id}/auth/generate", { + params: { path: { db_id: id } }, + body: { authorization: opts.authorization, expires_at: opts.expiresAt }, + }); + return data; +} + +/** RFC 3339 timestamp `minutes` from now (defaults to the token TTL). */ +export function tokenExpiryFromNow(minutes = TOKEN_TTL_MINUTES): string { + return new Date(Date.now() + minutes * 60 * 1000).toISOString(); +} + +/** Fetch live status metrics for the given database IDs. */ +export async function fetchLiveStatus( + client: DbClient, + ids: string[], +): Promise> { + const { data } = await client.POST("/v1/live/live_db", { + body: { db_ids: ids }, + }); + return data?.live_metrics ?? {}; +} + +/** "Active" when the database is live, otherwise "Idle". */ +export function liveStatusLabel(live: DBLiveStatus | undefined): string { + return live?.state === "Live" ? "Active" : "Idle"; +} + +/** Primary region code from live metadata, or null when not live. */ +export function liveMainRegion(live: DBLiveStatus | undefined): string | null { + return live?.state === "Live" ? live.metadata.main : null; +} diff --git a/packages/cli/src/commands/db/constants.ts b/packages/cli/src/commands/db/constants.ts index 3b7c8b8..b347d9b 100644 --- a/packages/cli/src/commands/db/constants.ts +++ b/packages/cli/src/commands/db/constants.ts @@ -10,6 +10,12 @@ export const ENV_DATABASE_AUTH_TOKEN = "BUNNY_DATABASE_AUTH_TOKEN"; /** Filename for the linked-database manifest stored under `.bunny/`. */ export const DATABASE_MANIFEST = "database.json"; +/** Page size used when paginating the database list endpoint. */ +export const DB_PAGE_SIZE = 100; + +/** Default lifetime for tokens minted by `db shell` / `db studio`. */ +export const TOKEN_TTL_MINUTES = 30; + /** Shape of `.bunny/database.json`. */ export interface DatabaseManifest { id: string; diff --git a/packages/cli/src/commands/db/create.ts b/packages/cli/src/commands/db/create.ts index 1b2cf9f..157a4dd 100644 --- a/packages/cli/src/commands/db/create.ts +++ b/packages/cli/src/commands/db/create.ts @@ -10,6 +10,7 @@ import { logger } from "../../core/logger.ts"; import { loadManifest, saveManifest } from "../../core/manifest.ts"; import { confirm, spinner } from "../../core/ui.ts"; import { readEnvValue, writeEnvValue } from "../../utils/env-file.ts"; +import { fetchRegionConfig, generateToken } from "./api.ts"; import { DATABASE_MANIFEST, type DatabaseManifest, @@ -146,16 +147,10 @@ export const dbCreateCommand = defineCommand({ const configSpin = spinner("Fetching available regions..."); configSpin.start(); - const { data: regionConfig } = await client.GET("/v1/config", { - params: {}, - }); + const regionConfig = await fetchRegionConfig(client); configSpin.stop(); - if (!regionConfig) { - throw new UserError("Could not fetch region configuration."); - } - const storageRegions = regionConfig.storage_region_available; const availablePrimary = regionConfig.primary_regions; const availableReplicas = regionConfig.replica_regions; @@ -258,7 +253,7 @@ export const dbCreateCommand = defineCommand({ message: "Database location:", choices, initial: preselected - ? choices.findIndex((c: any) => c.value === preselected) + ? choices.findIndex((c) => c.value === preselected) : 0, }); if (!location) throw new UserError("Location is required."); @@ -394,13 +389,10 @@ export const dbCreateCommand = defineCommand({ const tokenSpin = spinner("Generating token..."); tokenSpin.start(); - const { data: tokenData } = await client.PUT( - "/v2/databases/{db_id}/auth/generate", - { - params: { path: { db_id: data.db_id } }, - body: { authorization: "full-access", expires_at: null }, - }, - ); + const tokenData = await generateToken(client, data.db_id, { + authorization: "full-access", + expiresAt: null, + }); tokenSpin.stop(); diff --git a/packages/cli/src/commands/db/delete.ts b/packages/cli/src/commands/db/delete.ts index 1554763..e1f2db6 100644 --- a/packages/cli/src/commands/db/delete.ts +++ b/packages/cli/src/commands/db/delete.ts @@ -3,11 +3,11 @@ import prompts from "prompts"; import { resolveConfig } from "../../config/index.ts"; import { clientOptions } from "../../core/client-options.ts"; import { defineCommand } from "../../core/define-command.ts"; -import { UserError } from "../../core/errors.ts"; import { logger } from "../../core/logger.ts"; import { loadManifest, removeManifest } from "../../core/manifest.ts"; import { confirm, spinner } from "../../core/ui.ts"; import { readEnvValue, removeEnvValue } from "../../utils/env-file.ts"; +import { fetchDatabase } from "./api.ts"; import { ARG_DATABASE_ID, DATABASE_MANIFEST, @@ -93,15 +93,10 @@ export const dbDeleteCommand = defineCommand({ const fetchSpin = spinner("Fetching database..."); fetchSpin.start(); - const { data } = await client.GET("/v2/databases/{db_id}", { - params: { path: { db_id: databaseId } }, - }); + const db = await fetchDatabase(client, databaseId); fetchSpin.stop(); - const db = data?.db; - if (!db) throw new UserError(`Database ${databaseId} not found.`); - if (source === "env") { logger.dim(`Database: ${db.name} (${databaseId}, from .env)`); } else if (source === "manifest") { diff --git a/packages/cli/src/commands/db/link.ts b/packages/cli/src/commands/db/link.ts index 4d12769..0917296 100644 --- a/packages/cli/src/commands/db/link.ts +++ b/packages/cli/src/commands/db/link.ts @@ -1,5 +1,4 @@ import { createDbClient } from "@bunny.net/openapi-client"; -import type { components } from "@bunny.net/openapi-client/generated/database.d.ts"; import prompts from "prompts"; import { resolveConfig } from "../../config/index.ts"; import { clientOptions } from "../../core/client-options.ts"; @@ -8,14 +7,13 @@ import { UserError } from "../../core/errors.ts"; import { logger } from "../../core/logger.ts"; import { saveManifest } from "../../core/manifest.ts"; import { spinner } from "../../core/ui.ts"; +import { fetchAllDatabases, fetchDatabase } from "./api.ts"; import { ARG_DATABASE_ID, DATABASE_MANIFEST, type DatabaseManifest, } from "./constants.ts"; -type Database = Pick; - const COMMAND = `link [${ARG_DATABASE_ID}]`; const DESCRIPTION = "Link the current directory to a database."; @@ -66,17 +64,10 @@ export const dbLinkCommand = defineCommand({ const spin = spinner("Fetching database..."); spin.start(); - const { data } = await client.GET("/v2/databases/{db_id}", { - params: { path: { db_id: databaseIdArg } }, - }); + const db = await fetchDatabase(client, databaseIdArg); spin.stop(); - const db = data?.db; - if (!db) { - throw new UserError(`Database ${databaseIdArg} not found.`); - } - saveManifest(DATABASE_MANIFEST, { id: db.id, name: db.name, @@ -94,19 +85,7 @@ export const dbLinkCommand = defineCommand({ const spin = spinner("Fetching databases..."); spin.start(); - const allDatabases: Database[] = []; - let page = 1; - - while (true) { - const { data } = await client.GET("/v2/databases", { - params: { query: { page, per_page: 100 } }, - }); - - allDatabases.push(...(data?.databases ?? [])); - - if (!data?.page_info?.has_more_items) break; - page++; - } + const allDatabases = await fetchAllDatabases(client); spin.stop(); @@ -130,8 +109,7 @@ export const dbLinkCommand = defineCommand({ }); if (!selected) { - logger.log("Link cancelled."); - process.exit(1); + throw new UserError("Link cancelled."); } saveManifest(DATABASE_MANIFEST, { diff --git a/packages/cli/src/commands/db/list.ts b/packages/cli/src/commands/db/list.ts index a2e5252..b308083 100644 --- a/packages/cli/src/commands/db/list.ts +++ b/packages/cli/src/commands/db/list.ts @@ -1,15 +1,18 @@ import { createDbClient } from "@bunny.net/openapi-client"; -import type { components } from "@bunny.net/openapi-client/generated/database.d.ts"; import { resolveConfig } from "../../config/index.ts"; import { clientOptions } from "../../core/client-options.ts"; import { defineCommand } from "../../core/define-command.ts"; import { formatBytes, formatTable } from "../../core/format.ts"; import { logger } from "../../core/logger.ts"; import { spinner } from "../../core/ui.ts"; - -type Database = components["schemas"]["Database2"]; -type DBLiveStatus = components["schemas"]["DBLiveStatus"]; -type Region = components["schemas"]["Region"]; +import { + fetchAllDatabases, + fetchLiveStatus, + fetchRegionConfig, + liveMainRegion, + liveStatusLabel, + regionNameMap, +} from "./api.ts"; const COMMAND = "list"; const ALIASES = ["ls"] as const; @@ -46,40 +49,22 @@ export const dbListCommand = defineCommand({ const spin = spinner("Fetching databases..."); spin.start(); - const allDatabases: Database[] = []; - let page = 1; - - while (true) { - const { data } = await client.GET("/v2/databases", { - params: { query: { page, per_page: 100 } }, - }); - - allDatabases.push(...(data?.databases ?? [])); - - if (!data?.page_info?.has_more_items) break; - page++; - } + const allDatabases = await fetchAllDatabases(client); // Fetch live status and region config in parallel - let liveMetrics: Record = {}; - const regionNames = new Map(); + let liveMetrics: Awaited> = {}; + let regionNames = new Map(); if (allDatabases.length > 0) { - const [liveRes, configRes] = await Promise.all([ - client.POST("/v1/live/live_db", { - body: { db_ids: allDatabases.map((db) => db.id) }, - }), - client.GET("/v1/config", { params: {} }), + const [live, config] = await Promise.all([ + fetchLiveStatus( + client, + allDatabases.map((db) => db.id), + ), + fetchRegionConfig(client), ]); - liveMetrics = liveRes.data?.live_metrics ?? {}; - - const allRegions: Region[] = [ - ...(configRes.data?.primary_regions ?? []), - ...(configRes.data?.replica_regions ?? []), - ]; - for (const r of allRegions) { - regionNames.set(r.id, r.name); - } + liveMetrics = live; + regionNames = regionNameMap(config); } spin.stop(); @@ -101,8 +86,8 @@ export const dbListCommand = defineCommand({ ["ID", "Name", "Status", "Primary Region", "Size"], databases.map((db) => { const live = liveMetrics[db.id]; - const status = live?.state === "Live" ? "Active" : "Idle"; - const regionCode = live?.state === "Live" ? live.metadata.main : null; + const status = liveStatusLabel(live); + const regionCode = liveMainRegion(live); const primary = regionCode ? (regionNames.get(regionCode) ?? regionCode) : "—"; diff --git a/packages/cli/src/commands/db/quickstart-snippets.ts b/packages/cli/src/commands/db/quickstart-snippets.ts new file mode 100644 index 0000000..b8556b8 --- /dev/null +++ b/packages/cli/src/commands/db/quickstart-snippets.ts @@ -0,0 +1,110 @@ +import { ENV_DATABASE_AUTH_TOKEN, ENV_DATABASE_URL } from "./constants.ts"; + +export const QUICKSTART_LANGUAGES = [ + { id: "typescript", title: "TypeScript" }, + { id: "go", title: "Go" }, + { id: "rust", title: "Rust" }, + { id: "dotnet", title: ".NET" }, +] as const; + +export type QuickstartLang = (typeof QUICKSTART_LANGUAGES)[number]["id"]; + +export interface Snippet { + lang: string; + install: string; + code: string; +} + +interface SnippetVars { + urlEnv: string; + tokenEnv: string; +} + +const TEMPLATES: Record Snippet> = { + typescript: ({ urlEnv, tokenEnv }) => ({ + lang: "TypeScript", + install: "bun add @libsql/client", + code: `import { createClient } from "@libsql/client/web"; + +const client = createClient({ + url: process.env.${urlEnv}, + authToken: process.env.${tokenEnv}, +}); + +await client.execute("SELECT * FROM users");`, + }), + go: ({ urlEnv, tokenEnv }) => ({ + lang: "Go", + install: "go get github.com/tursodatabase/libsql-client-go/libsql", + code: `package main + +import ( +\t"database/sql" +\t"fmt" +\t"os" + +\t_ "github.com/tursodatabase/libsql-client-go/libsql" +) + +func main() { +\turl := fmt.Sprintf("%s?authToken=%s", +\t\tos.Getenv("${urlEnv}"), +\t\tos.Getenv("${tokenEnv}"), +\t) + +\tdb, err := sql.Open("libsql", url) +\tif err != nil { +\t\tfmt.Fprintf(os.Stderr, "failed to open db %s: %s", url, err) +\t\tos.Exit(1) +\t} +\tdefer db.Close() +}`, + }), + rust: ({ urlEnv, tokenEnv }) => ({ + lang: "Rust", + install: "cargo add libsql", + code: `use libsql::Builder; + +let url = std::env::var("${urlEnv}").expect("${urlEnv} must be set"); +let token = std::env::var("${tokenEnv}").expect("${tokenEnv} must be set"); + +let db = Builder::new_remote(url, token) + .build() + .await?; + +let conn = db.connect()?; + +let mut rows = conn.query("SELECT * FROM users", ()).await?; + +while let Some(row) = rows.next().await? { + let id: i64 = row.get(0)?; + let name: String = row.get(1)?; + println!("User: {} - {}", id, name); +}`, + }), + dotnet: ({ urlEnv, tokenEnv }) => ({ + lang: ".NET", + install: "dotnet add package Bunny.LibSql.Client", + code: `var db = new AppDb( + Environment.GetEnvironmentVariable("${urlEnv}"), + Environment.GetEnvironmentVariable("${tokenEnv}") +); + +await db.ApplyMigrationsAsync(); + +var users = await db.Users.ToListAsync(); + +foreach (var user in users) +{ + Console.WriteLine($"User: {user.name}"); +}`, + }), +}; + +/** Return the install command and connection code snippet for a language. */ +export function getSnippet(lang: QuickstartLang): Snippet { + return TEMPLATES[lang]({ + urlEnv: ENV_DATABASE_URL, + tokenEnv: ENV_DATABASE_AUTH_TOKEN, + }); +} diff --git a/packages/cli/src/commands/db/quickstart.ts b/packages/cli/src/commands/db/quickstart.ts index 40073cf..9db9d81 100644 --- a/packages/cli/src/commands/db/quickstart.ts +++ b/packages/cli/src/commands/db/quickstart.ts @@ -8,7 +8,17 @@ import { UserError } from "../../core/errors.ts"; import { logger } from "../../core/logger.ts"; import { spinner } from "../../core/ui.ts"; import { readEnvValue } from "../../utils/env-file.ts"; -import { ARG_DATABASE_ID } from "./constants.ts"; +import { generateToken } from "./api.ts"; +import { + ARG_DATABASE_ID, + ENV_DATABASE_AUTH_TOKEN, + ENV_DATABASE_URL, +} from "./constants.ts"; +import { + getSnippet, + QUICKSTART_LANGUAGES, + type QuickstartLang, +} from "./quickstart-snippets.ts"; import { resolveDbId } from "./resolve-db.ts"; const COMMAND = `quickstart [${ARG_DATABASE_ID}]`; @@ -19,114 +29,6 @@ const ARG_LANG_ALIAS = "l"; const ARG_URL = "url"; const ARG_TOKEN = "token"; -interface Snippet { - lang: string; - install: string; - code: string; -} - -const LANGUAGES = [ - { id: "typescript", title: "TypeScript" }, - { id: "go", title: "Go" }, - { id: "rust", title: "Rust" }, - { id: "dotnet", title: ".NET" }, -] as const; - -type LangId = (typeof LANGUAGES)[number]["id"]; - -/** Return the install command and connection code snippet for a given language. */ -function getSnippet(lang: string): Snippet { - switch (lang) { - case "typescript": - return { - lang: "TypeScript", - install: "bun add @libsql/client", - code: `import { createClient } from "@libsql/client/web"; - -const client = createClient({ - url: process.env.BUNNY_DATABASE_URL, - authToken: process.env.BUNNY_DATABASE_AUTH_TOKEN, -}); - -await client.execute("SELECT * FROM users");`, - }; - case "go": - return { - lang: "Go", - install: "go get github.com/tursodatabase/libsql-client-go/libsql", - code: `package main - -import ( -\t"database/sql" -\t"fmt" -\t"os" - -\t_ "github.com/tursodatabase/libsql-client-go/libsql" -) - -func main() { -\turl := fmt.Sprintf("%s?authToken=%s", -\t\tos.Getenv("BUNNY_DATABASE_URL"), -\t\tos.Getenv("BUNNY_DATABASE_AUTH_TOKEN"), -\t) - -\tdb, err := sql.Open("libsql", url) -\tif err != nil { -\t\tfmt.Fprintf(os.Stderr, "failed to open db %s: %s", url, err) -\t\tos.Exit(1) -\t} -\tdefer db.Close() -}`, - }; - case "rust": - return { - lang: "Rust", - install: "cargo add libsql", - code: `use libsql::Builder; - -let url = std::env::var("BUNNY_DATABASE_URL").expect("BUNNY_DATABASE_URL must be set"); -let token = std::env::var("BUNNY_DATABASE_AUTH_TOKEN").expect("BUNNY_DATABASE_AUTH_TOKEN must be set"); - -let db = Builder::new_remote(url, token) - .build() - .await?; - -let conn = db.connect()?; - -let mut rows = conn.query("SELECT * FROM users", ()).await?; - -while let Some(row) = rows.next().await? { - let id: i64 = row.get(0)?; - let name: String = row.get(1)?; - println!("User: {} - {}", id, name); -}`, - }; - case "dotnet": - return { - lang: ".NET", - install: "dotnet add package Bunny.LibSql.Client", - code: `var db = new AppDb( - Environment.GetEnvironmentVariable("BUNNY_DATABASE_URL"), - Environment.GetEnvironmentVariable("BUNNY_DATABASE_AUTH_TOKEN") -); - -await db.ApplyMigrationsAsync(); - -var users = await db.Users.ToListAsync(); - -foreach (var user in users) -{ - Console.WriteLine($"User: {user.name}"); -}`, - }; - default: - throw new UserError( - `Unsupported language: "${lang}"`, - `Supported: ${LANGUAGES.map((l) => l.id).join(", ")}`, - ); - } -} - /** * Generate a language-specific quickstart guide for connecting to a database. * @@ -179,7 +81,7 @@ export const dbQuickstartCommand = defineCommand<{ .option(ARG_LANG, { alias: ARG_LANG_ALIAS, type: "string", - choices: LANGUAGES.map((l) => l.id) as string[], + choices: QUICKSTART_LANGUAGES.map((l) => l.id) as string[], describe: "Language for the code snippet", }) .option(ARG_URL, { @@ -202,13 +104,15 @@ export const dbQuickstartCommand = defineCommand<{ apiKey, }) => { // Language selection - let lang: LangId | undefined = langArg as LangId | undefined; + let lang: QuickstartLang | undefined = langArg as + | QuickstartLang + | undefined; if (!lang) { const { value } = await prompts({ type: "select", name: "value", message: "Language:", - choices: LANGUAGES.map((l) => ({ + choices: QUICKSTART_LANGUAGES.map((l) => ({ title: l.title, value: l.id, })), @@ -242,9 +146,9 @@ export const dbQuickstartCommand = defineCommand<{ if (!token) { spin.text = "Generating token..."; fetches.push( - client.PUT("/v2/databases/{db_id}/auth/generate", { - params: { path: { db_id: databaseId } }, - body: { authorization: "full-access", expires_at: null }, + generateToken(client, databaseId, { + authorization: "full-access", + expiresAt: null, }), ); } @@ -256,7 +160,7 @@ export const dbQuickstartCommand = defineCommand<{ const db = dbResult.data?.db; dbName = db?.name; if (!url) url = db?.url; - if (!token && tokenResult) token = tokenResult.data?.token; + if (!token && tokenResult) token = tokenResult.token; } if (!url || !token) { @@ -274,8 +178,8 @@ export const dbQuickstartCommand = defineCommand<{ install: snippet.install, code: snippet.code, env: { - BUNNY_DATABASE_URL: url, - BUNNY_DATABASE_AUTH_TOKEN: token, + [ENV_DATABASE_URL]: url, + [ENV_DATABASE_AUTH_TOKEN]: token, }, }, null, @@ -288,8 +192,8 @@ export const dbQuickstartCommand = defineCommand<{ logger.info(`Quickstart for ${dbName ?? "database"} (${snippet.lang})`); logger.log(); - const hasUrl = !!readEnvValue("BUNNY_DATABASE_URL"); - const hasToken = !!readEnvValue("BUNNY_DATABASE_AUTH_TOKEN"); + const hasUrl = !!readEnvValue(ENV_DATABASE_URL); + const hasToken = !!readEnvValue(ENV_DATABASE_AUTH_TOKEN); const envReady = hasUrl && hasToken; let step = 1; @@ -298,8 +202,8 @@ export const dbQuickstartCommand = defineCommand<{ logger.log(chalk.bold(` ${step}. Add to your .env`)); logger.log(); logger.log(chalk.gray(" # .env")); - if (!hasUrl) logger.log(` BUNNY_DATABASE_URL=${url}`); - if (!hasToken) logger.log(` BUNNY_DATABASE_AUTH_TOKEN=${token}`); + if (!hasUrl) logger.log(` ${ENV_DATABASE_URL}=${url}`); + if (!hasToken) logger.log(` ${ENV_DATABASE_AUTH_TOKEN}=${token}`); logger.log(); step++; } diff --git a/packages/cli/src/commands/db/regions/add.ts b/packages/cli/src/commands/db/regions/add.ts index d52397f..ec33709 100644 --- a/packages/cli/src/commands/db/regions/add.ts +++ b/packages/cli/src/commands/db/regions/add.ts @@ -4,16 +4,15 @@ import prompts from "prompts"; import { resolveConfig } from "../../../config/index.ts"; import { clientOptions } from "../../../core/client-options.ts"; import { defineCommand } from "../../../core/define-command.ts"; -import { UserError } from "../../../core/errors.ts"; import { formatTable } from "../../../core/format.ts"; import { logger } from "../../../core/logger.ts"; import { spinner } from "../../../core/ui.ts"; +import { fetchDatabaseWithRegions, regionNameMap } from "../api.ts"; import { ARG_DATABASE_ID } from "../constants.ts"; import { groupedRegionChoices } from "../region-choices.ts"; import { resolveDbId } from "../resolve-db.ts"; type PossibleRegion = components["schemas"]["PossibleRegion"]; -type Region = components["schemas"]["Region"]; const COMMAND = `add [${ARG_DATABASE_ID}]`; const DESCRIPTION = "Add regions to a database."; @@ -90,23 +89,13 @@ export const dbRegionsAddCommand = defineCommand({ const spin = spinner("Fetching database and regions..."); spin.start(); - const [dbResult, configResult] = await Promise.all([ - client.GET("/v2/databases/{db_id}", { - params: { path: { db_id: databaseId } }, - }), - client.GET("/v1/config", { params: {} }), - ]); + const { db, config: regionConfig } = await fetchDatabaseWithRegions( + client, + databaseId, + ); spin.stop(); - const db = dbResult.data?.db; - if (!db) throw new UserError(`Database ${databaseId} not found.`); - - const regionConfig = configResult.data; - if (!regionConfig) { - throw new UserError("Could not fetch region configuration."); - } - const availablePrimary = regionConfig.primary_regions; const availableReplicas = regionConfig.replica_regions; @@ -197,12 +186,7 @@ export const dbRegionsAddCommand = defineCommand({ return; } - // Build region name lookup - const regionNames = new Map(); - const allRegions: Region[] = [...availablePrimary, ...availableReplicas]; - for (const r of allRegions) { - regionNames.set(r.id, r.name); - } + const regionNames = regionNameMap(regionConfig); const added: string[][] = []; for (const id of newPrimary) { diff --git a/packages/cli/src/commands/db/regions/list.ts b/packages/cli/src/commands/db/regions/list.ts index 87a8187..16333e4 100644 --- a/packages/cli/src/commands/db/regions/list.ts +++ b/packages/cli/src/commands/db/regions/list.ts @@ -1,17 +1,14 @@ import { createDbClient } from "@bunny.net/openapi-client"; -import type { components } from "@bunny.net/openapi-client/generated/database.d.ts"; import { resolveConfig } from "../../../config/index.ts"; import { clientOptions } from "../../../core/client-options.ts"; import { defineCommand } from "../../../core/define-command.ts"; -import { UserError } from "../../../core/errors.ts"; import { formatTable } from "../../../core/format.ts"; import { logger } from "../../../core/logger.ts"; import { spinner } from "../../../core/ui.ts"; +import { fetchDatabaseWithRegions, regionNameMap } from "../api.ts"; import { ARG_DATABASE_ID } from "../constants.ts"; import { resolveDbId } from "../resolve-db.ts"; -type Region = components["schemas"]["Region"]; - const COMMAND = `list [${ARG_DATABASE_ID}]`; const ALIASES = ["ls"] as const; const DESCRIPTION = "List configured regions for a database."; @@ -54,26 +51,14 @@ export const dbRegionsListCommand = defineCommand({ const spin = spinner("Fetching regions..."); spin.start(); - const [dbResult, configResult] = await Promise.all([ - client.GET("/v2/databases/{db_id}", { - params: { path: { db_id: databaseId } }, - }), - client.GET("/v1/config", { params: {} }), - ]); + const { db, config: regionConfig } = await fetchDatabaseWithRegions( + client, + databaseId, + ); spin.stop(); - const db = dbResult.data?.db; - if (!db) throw new UserError(`Database ${databaseId} not found.`); - - const regionNames = new Map(); - const allRegions: Region[] = [ - ...(configResult.data?.primary_regions ?? []), - ...(configResult.data?.replica_regions ?? []), - ]; - for (const r of allRegions) { - regionNames.set(r.id, r.name); - } + const regionNames = regionNameMap(regionConfig); if (output === "json") { logger.log( diff --git a/packages/cli/src/commands/db/regions/remove.ts b/packages/cli/src/commands/db/regions/remove.ts index c28935a..3e56583 100644 --- a/packages/cli/src/commands/db/regions/remove.ts +++ b/packages/cli/src/commands/db/regions/remove.ts @@ -1,5 +1,4 @@ import { createDbClient } from "@bunny.net/openapi-client"; -import type { components } from "@bunny.net/openapi-client/generated/database.d.ts"; import prompts from "prompts"; import { resolveConfig } from "../../../config/index.ts"; import { clientOptions } from "../../../core/client-options.ts"; @@ -8,11 +7,10 @@ import { UserError } from "../../../core/errors.ts"; import { formatTable } from "../../../core/format.ts"; import { logger } from "../../../core/logger.ts"; import { confirm, spinner } from "../../../core/ui.ts"; +import { fetchDatabaseWithRegions, regionNameMap } from "../api.ts"; import { ARG_DATABASE_ID } from "../constants.ts"; import { resolveDbId } from "../resolve-db.ts"; -type Region = components["schemas"]["Region"]; - const COMMAND = `remove [${ARG_DATABASE_ID}]`; const ALIASES = ["rm"] as const; const DESCRIPTION = "Remove regions from a database."; @@ -98,27 +96,14 @@ export const dbRegionsRemoveCommand = defineCommand({ const spin = spinner("Fetching database and regions..."); spin.start(); - const [dbResult, configResult] = await Promise.all([ - client.GET("/v2/databases/{db_id}", { - params: { path: { db_id: databaseId } }, - }), - client.GET("/v1/config", { params: {} }), - ]); + const { db, config: regionConfig } = await fetchDatabaseWithRegions( + client, + databaseId, + ); spin.stop(); - const db = dbResult.data?.db; - if (!db) throw new UserError(`Database ${databaseId} not found.`); - - // Build region name lookup - const regionNames = new Map(); - const allRegions: Region[] = [ - ...(configResult.data?.primary_regions ?? []), - ...(configResult.data?.replica_regions ?? []), - ]; - for (const r of allRegions) { - regionNames.set(r.id, r.name); - } + const regionNames = regionNameMap(regionConfig); let removePrimary: Set; let removeReplicas: Set; diff --git a/packages/cli/src/commands/db/regions/update.ts b/packages/cli/src/commands/db/regions/update.ts index 4ededc3..84cfb43 100644 --- a/packages/cli/src/commands/db/regions/update.ts +++ b/packages/cli/src/commands/db/regions/update.ts @@ -8,12 +8,12 @@ import { UserError } from "../../../core/errors.ts"; import { formatTable } from "../../../core/format.ts"; import { logger } from "../../../core/logger.ts"; import { spinner } from "../../../core/ui.ts"; +import { fetchDatabaseWithRegions, regionNameMap } from "../api.ts"; import { ARG_DATABASE_ID } from "../constants.ts"; import { groupedRegionChoices } from "../region-choices.ts"; import { resolveDbId } from "../resolve-db.ts"; type PossibleRegion = components["schemas"]["PossibleRegion"]; -type Region = components["schemas"]["Region"]; const COMMAND = `update [${ARG_DATABASE_ID}]`; const DESCRIPTION = "Update region configuration."; @@ -78,23 +78,13 @@ export const dbRegionsUpdateCommand = defineCommand({ const spin = spinner("Fetching database and regions..."); spin.start(); - const [dbResult, configResult] = await Promise.all([ - client.GET("/v2/databases/{db_id}", { - params: { path: { db_id: databaseId } }, - }), - client.GET("/v1/config", { params: {} }), - ]); + const { db, config: regionConfig } = await fetchDatabaseWithRegions( + client, + databaseId, + ); spin.stop(); - const db = dbResult.data?.db; - if (!db) throw new UserError(`Database ${databaseId} not found.`); - - const regionConfig = configResult.data; - if (!regionConfig) { - throw new UserError("Could not fetch region configuration."); - } - const availablePrimary = regionConfig.primary_regions; const availableReplicas = regionConfig.replica_regions; @@ -123,8 +113,7 @@ export const dbRegionsUpdateCommand = defineCommand({ }); if (!selectedPrimary) { - logger.log("Cancelled."); - return; + throw new UserError("Cancelled."); } newPrimary = selectedPrimary as PossibleRegion[]; @@ -138,8 +127,7 @@ export const dbRegionsUpdateCommand = defineCommand({ }); if (!selectedReplicas) { - logger.log("Cancelled."); - return; + throw new UserError("Cancelled."); } newReplicas = selectedReplicas as PossibleRegion[]; @@ -194,11 +182,7 @@ export const dbRegionsUpdateCommand = defineCommand({ } // Build region name lookup - const regionNames = new Map(); - const allRegions: Region[] = [...availablePrimary, ...availableReplicas]; - for (const r of allRegions) { - regionNames.set(r.id, r.name); - } + const regionNames = regionNameMap(regionConfig); const rows: string[][] = []; for (const id of newPrimary) { diff --git a/packages/cli/src/commands/db/resolve-db.ts b/packages/cli/src/commands/db/resolve-db.ts index 97d810b..9aa211b 100644 --- a/packages/cli/src/commands/db/resolve-db.ts +++ b/packages/cli/src/commands/db/resolve-db.ts @@ -1,26 +1,26 @@ import type { createDbClient } from "@bunny.net/openapi-client"; import type { components } from "@bunny.net/openapi-client/generated/database.d.ts"; - -type Database = Pick; - -export interface ResolvedDb { - id: Database["id"]; - /** Database name when known from the manifest, env lookup, or prompt selection. */ - name?: Database["name"]; - source: "argument" | "manifest" | "env" | "prompt"; -} - import prompts from "prompts"; import { UserError } from "../../core/errors.ts"; import { loadManifest } from "../../core/manifest.ts"; import { spinner } from "../../core/ui.ts"; import { readEnvValue } from "../../utils/env-file.ts"; +import { fetchAllDatabases } from "./api.ts"; import { DATABASE_MANIFEST, type DatabaseManifest, ENV_DATABASE_URL, } from "./constants.ts"; +type Database = Pick; + +export interface ResolvedDb { + id: Database["id"]; + /** Database name when known from the manifest, env lookup, or prompt selection. */ + name?: Database["name"]; + source: "argument" | "manifest" | "env" | "prompt"; +} + /** * Walk up the directory tree looking for a `.env` file containing a database URL. * Returns the URL value or `undefined` if not found. @@ -53,23 +53,10 @@ export async function resolveDbId( const url = findDbUrlFromEnv(); - // Paginate through all databases - const allDatabases: Database[] = []; - let page = 1; - const spin = url ? undefined : spinner("Fetching databases..."); spin?.start(); - while (true) { - const { data } = await client.GET("/v2/databases", { - params: { query: { page, per_page: 100 } }, - }); - - allDatabases.push(...(data?.databases ?? [])); - - if (!data?.page_info?.has_more_items) break; - page++; - } + const allDatabases = await fetchAllDatabases(client); spin?.stop(); @@ -103,7 +90,7 @@ export async function resolveDbId( }); if (!selected) { - process.exit(1); + throw new UserError("No database selected."); } return { id: selected.id, name: selected.name, source: "prompt" }; diff --git a/packages/cli/src/commands/db/shell.ts b/packages/cli/src/commands/db/shell.ts index e29b967..9bcbf65 100644 --- a/packages/cli/src/commands/db/shell.ts +++ b/packages/cli/src/commands/db/shell.ts @@ -9,10 +9,12 @@ import { UserError } from "../../core/errors.ts"; import { logger } from "../../core/logger.ts"; import { spinner } from "../../core/ui.ts"; import { readEnvValue } from "../../utils/env-file.ts"; +import { generateToken, tokenExpiryFromNow } from "./api.ts"; import { ARG_DATABASE_ID, ENV_DATABASE_AUTH_TOKEN, ENV_DATABASE_URL, + TOKEN_TTL_MINUTES, } from "./constants.ts"; import { resolveDbId } from "./resolve-db.ts"; @@ -30,8 +32,6 @@ const ARG_VIEWS_DIR = "views-dir"; const PRINT_MODES = ["default", "table", "json", "csv", "markdown"]; -const TOKEN_TTL_MINUTES = 30; - /** Create a ShellLogger adapter that wraps the CLI logger. */ function shellLogger(): ShellLogger { return { @@ -94,13 +94,10 @@ async function resolveCredentials( if (willGenerateToken) { spin.text = "Generating token..."; - const expiresAt = new Date( - Date.now() + TOKEN_TTL_MINUTES * 60 * 1000, - ).toISOString(); fetches.push( - apiClient.PUT("/v2/databases/{db_id}/auth/generate", { - params: { path: { db_id: databaseId } }, - body: { authorization: "full-access", expires_at: expiresAt }, + generateToken(apiClient, databaseId, { + authorization: "full-access", + expiresAt: tokenExpiryFromNow(), }), ); } @@ -110,7 +107,7 @@ async function resolveCredentials( spin.stop(); if (!url && dbResult) url = dbResult.data?.db?.url; - if (willGenerateToken && tokenResult) token = tokenResult.data?.token; + if (willGenerateToken && tokenResult) token = tokenResult.token; if (!url || !token) { throw new UserError("Could not resolve database URL or generate token."); @@ -251,8 +248,8 @@ export const dbShellCommand = defineCommand<{ masked: !unmaskArg, logger: log, }); - } catch (err: any) { - throw new UserError(err.message); + } catch (err: unknown) { + throw new UserError(err instanceof Error ? err.message : String(err)); } } return; @@ -268,9 +265,9 @@ export const dbShellCommand = defineCommand<{ databaseId: resolvedDbId, viewsDir: viewsDirArg ? resolve(viewsDirArg) : undefined, }); - } catch (err: any) { + } catch (err: unknown) { throw new UserError( - err.message, + err instanceof Error ? err.message : String(err), `Use --${ARG_EXEC} to run a statement non-interactively.`, ); } diff --git a/packages/cli/src/commands/db/show.ts b/packages/cli/src/commands/db/show.ts index 16b4fb8..bbd7420 100644 --- a/packages/cli/src/commands/db/show.ts +++ b/packages/cli/src/commands/db/show.ts @@ -1,17 +1,21 @@ import { createDbClient } from "@bunny.net/openapi-client"; -import type { components } from "@bunny.net/openapi-client/generated/database.d.ts"; import { resolveConfig } from "../../config/index.ts"; import { clientOptions } from "../../core/client-options.ts"; import { defineCommand } from "../../core/define-command.ts"; -import { UserError } from "../../core/errors.ts"; import { formatBytes, formatKeyValue, progressBar } from "../../core/format.ts"; import { logger } from "../../core/logger.ts"; import { spinner } from "../../core/ui.ts"; +import { + fetchDatabase, + fetchLiveStatus, + fetchRegionConfig, + liveMainRegion, + liveStatusLabel, + regionNameMap, +} from "./api.ts"; import { ARG_DATABASE_ID } from "./constants.ts"; import { resolveDbId } from "./resolve-db.ts"; -type Region = components["schemas"]["Region"]; - const COMMAND = `show [${ARG_DATABASE_ID}]`; const DESCRIPTION = "Show database details."; @@ -60,31 +64,16 @@ export const dbShowCommand = defineCommand({ const spin = spinner("Fetching database..."); spin.start(); - const [dbResult, liveResult, configResult] = await Promise.all([ - client.GET("/v2/databases/{db_id}", { - params: { path: { db_id: databaseId } }, - }), - client.POST("/v1/live/live_db", { - body: { db_ids: [databaseId] }, - }), - client.GET("/v1/config", { params: {} }), + const [db, liveMetrics, regionConfig] = await Promise.all([ + fetchDatabase(client, databaseId), + fetchLiveStatus(client, [databaseId]), + fetchRegionConfig(client), ]); spin.stop(); - const db = dbResult.data?.db; - if (!db) throw new UserError(`Database ${databaseId} not found.`); - - const live = liveResult.data?.live_metrics?.[databaseId]; - - const regionNames = new Map(); - const allRegions: Region[] = [ - ...(configResult.data?.primary_regions ?? []), - ...(configResult.data?.replica_regions ?? []), - ]; - for (const r of allRegions) { - regionNames.set(r.id, r.name); - } + const live = liveMetrics[databaseId]; + const regionNames = regionNameMap(regionConfig); /** Format a region code as "Name (CODE)". */ const formatRegion = (code: string) => { @@ -97,9 +86,9 @@ export const dbShowCommand = defineCommand({ return; } - const status = live?.state === "Live" ? "Active" : "Idle"; - const primaryRegion = - live?.state === "Live" ? formatRegion(live.metadata.main) : "—"; + const status = liveStatusLabel(live); + const mainCode = liveMainRegion(live); + const primaryRegion = mainCode ? formatRegion(mainCode) : "—"; const replicaRegions = live?.state === "Live" && live.metadata.replicas.length > 0 ? live.metadata.replicas.map(formatRegion).join(", ") diff --git a/packages/cli/src/commands/db/studio.ts b/packages/cli/src/commands/db/studio.ts index 81af773..ea571fb 100644 --- a/packages/cli/src/commands/db/studio.ts +++ b/packages/cli/src/commands/db/studio.ts @@ -6,10 +6,12 @@ import { UserError } from "../../core/errors.ts"; import { logger } from "../../core/logger.ts"; import { confirm, spinner } from "../../core/ui.ts"; import { readEnvValue } from "../../utils/env-file.ts"; +import { generateToken, tokenExpiryFromNow } from "./api.ts"; import { ARG_DATABASE_ID, ENV_DATABASE_AUTH_TOKEN, ENV_DATABASE_URL, + TOKEN_TTL_MINUTES, } from "./constants.ts"; import { resolveDbId } from "./resolve-db.ts"; @@ -24,8 +26,6 @@ const ARG_DEV = "dev"; const ARG_FORCE = "force"; const ARG_FORCE_ALIAS = "f"; -const TOKEN_TTL_MINUTES = 30; - /** * Resolve database credentials — same pattern as shell.ts. */ @@ -64,13 +64,10 @@ async function resolveCredentials( if (!token) { spin.text = "Generating token..."; - const expiresAt = new Date( - Date.now() + TOKEN_TTL_MINUTES * 60 * 1000, - ).toISOString(); fetches.push( - apiClient.PUT("/v2/databases/{db_id}/auth/generate", { - params: { path: { db_id: databaseId } }, - body: { authorization: "full-access", expires_at: expiresAt }, + generateToken(apiClient, databaseId, { + authorization: "full-access", + expiresAt: tokenExpiryFromNow(), }), ); } @@ -80,7 +77,7 @@ async function resolveCredentials( spin.stop(); if (!url && dbResult) url = dbResult.data?.db?.url; - if (!token && tokenResult) token = tokenResult.data?.token; + if (!token && tokenResult) token = tokenResult.token; if (!url || !token) { throw new UserError("Could not resolve database URL or generate token."); diff --git a/packages/cli/src/commands/db/tokens/create.ts b/packages/cli/src/commands/db/tokens/create.ts index 52a8ff9..86a6555 100644 --- a/packages/cli/src/commands/db/tokens/create.ts +++ b/packages/cli/src/commands/db/tokens/create.ts @@ -7,6 +7,7 @@ import { formatKeyValue } from "../../../core/format.ts"; import { logger } from "../../../core/logger.ts"; import { confirm, spinner } from "../../../core/ui.ts"; import { readEnvValue, writeEnvValue } from "../../../utils/env-file.ts"; +import { generateToken } from "../api.ts"; import { ARG_DATABASE_ID, ENV_DATABASE_AUTH_TOKEN, @@ -182,10 +183,7 @@ export const dbTokensCreateCommand = defineCommand<{ // Fetch token and database details in parallel const [tokenResult, dbResult] = await Promise.all([ - client.PUT("/v2/databases/{db_id}/auth/generate", { - params: { path: { db_id: databaseId } }, - body: { authorization, expires_at: expiresAt }, - }), + generateToken(client, databaseId, { authorization, expiresAt }), client.GET("/v2/databases/{db_id}", { params: { path: { db_id: databaseId } }, }), @@ -193,7 +191,7 @@ export const dbTokensCreateCommand = defineCommand<{ spin.stop(); - const token = tokenResult.data?.token; + const token = tokenResult?.token; const dbUrl = dbResult.data?.db?.url; if (output === "json") { @@ -201,7 +199,7 @@ export const dbTokensCreateCommand = defineCommand<{ JSON.stringify( { token, - expires_at: tokenResult.data?.expires_at ?? null, + expires_at: tokenResult?.expires_at ?? null, db_id: databaseId, authorization, }, @@ -226,7 +224,7 @@ export const dbTokensCreateCommand = defineCommand<{ } entries.push({ key: "Expires", - value: tokenResult.data?.expires_at ?? "never", + value: tokenResult?.expires_at ?? "never", }); logger.success("Token generated."); diff --git a/packages/cli/src/commands/db/tokens/invalidate.ts b/packages/cli/src/commands/db/tokens/invalidate.ts index ce1c2e5..a905071 100644 --- a/packages/cli/src/commands/db/tokens/invalidate.ts +++ b/packages/cli/src/commands/db/tokens/invalidate.ts @@ -10,6 +10,7 @@ import { removeEnvValue, writeEnvValue, } from "../../../utils/env-file.ts"; +import { generateToken } from "../api.ts"; import { ARG_DATABASE_ID, ENV_DATABASE_AUTH_TOKEN, @@ -187,9 +188,9 @@ export const dbTokensInvalidateCommand = defineCommand<{ spin3.start(); const [tokenResult, dbResult] = await Promise.all([ - client.PUT("/v2/databases/{db_id}/auth/generate", { - params: { path: { db_id: databaseId } }, - body: { authorization: "full-access", expires_at: null }, + generateToken(client, databaseId, { + authorization: "full-access", + expiresAt: null, }), client.GET("/v2/databases/{db_id}", { params: { path: { db_id: databaseId } }, @@ -198,7 +199,7 @@ export const dbTokensInvalidateCommand = defineCommand<{ spin3.stop(); - const newToken = tokenResult.data?.token; + const newToken = tokenResult?.token; const dbUrl = dbResult.data?.db?.url; if (!newToken) { diff --git a/packages/cli/src/commands/scripts/env/remove.ts b/packages/cli/src/commands/scripts/env/remove.ts index 415239c..a6a8118 100644 --- a/packages/cli/src/commands/scripts/env/remove.ts +++ b/packages/cli/src/commands/scripts/env/remove.ts @@ -119,8 +119,7 @@ export const scriptsEnvRemoveCommand = defineCommand({ name = value; } if (!name) { - logger.log("Cancelled."); - return; + throw new UserError("Cancelled."); } const entry = entries.find( diff --git a/packages/cli/src/commands/scripts/link.ts b/packages/cli/src/commands/scripts/link.ts index 93f93c7..f907f7b 100644 --- a/packages/cli/src/commands/scripts/link.ts +++ b/packages/cli/src/commands/scripts/link.ts @@ -111,8 +111,7 @@ export const scriptsLinkCommand = defineCommand({ }); if (!selected) { - logger.log("Link cancelled."); - return; + throw new UserError("Link cancelled."); } saveManifest(SCRIPT_MANIFEST, {