Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/prompt-eof-spin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bunny.net/cli": patch
---

fix(core): prompts no longer spin at 100% CPU forever when run without a terminal (CI, cron, `< /dev/null`); prompts now require an interactive terminal, so input prompts and destructive confirmations fail fast with exit 1 and a hint naming the flag to pass (`--force` or the value flag), offer-style prompts decline and continue, and piped prompt answers are no longer supported (pass flags instead)
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ bunny-cli/
│ │ ├── stats.ts # Shared stats rendering: sumChart(), renderBarChart(), formatBucketLabel() (UTC date labels), BAR_WIDTH (used by dns/zone/stats + scripts/stats)
│ │ ├── stats.test.ts # Tests for stats helpers
│ │ ├── types.ts # GlobalArgs, OutputFormat, and shared type definitions
│ │ ├── ui.ts # readPassword(), confirm(), confirmTyped(), requireConfirmable() (unattended runs must pass --force instead of hanging on a prompt), spinner() wrappers
│ │ ├── ui.ts # prompts() terminal-safe wrapper (always import it from here, never from the prompts package: raw prompts spins at 100% CPU when stdin hits EOF; the wrapper refuses non-TTY stdin up front, piped answers unsupported, prompts.inject() exempt), readPassword(), confirm() (gates throw with exit 1 when unanswerable; pass optional: true for offer prompts that should decline and continue), confirmTyped(), requireConfirmable() (unattended runs must pass --force instead of hanging on a prompt), spinner() wrappers
│ │ ├── ui.test.ts # Tests for requireConfirmable (no-TTY guard, --force bypass)
│ │ └── version.ts # VERSION constant from package.json
│ │
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ This is a Bun workspace monorepo with five packages:
- Use `formatTable()` / `formatKeyValue()` from `packages/cli/src/core/format.ts` for non-JSON output.
- Handle `--output json` first in every handler, then pass `output` to format functions.
- Use `logger` from `packages/cli/src/core/logger.ts` for all user-facing output.
- Import `prompts` from `packages/cli/src/core/ui.ts`, never from the `prompts` package directly: the wrapper refuses to prompt when stdin is not an interactive terminal (the raw library spins at 100% CPU on a closed stdin). Piped prompt answers are deliberately unsupported; flags and `--force` are the automation contract. `prompts.inject()` in tests still works.
- `confirm()` is a gate by default: if stdin closes before an answer it throws, so the command exits non-zero. Pass `optional: true` only for offer-style prompts (link this directory? save to .env?) where declining is a normal outcome and the command should continue.
- Throw `UserError` for expected errors.
- Import API clients from `@bunny.net/openapi-client`, not relative paths. Import generated types from the per-API entrypoints (`@bunny.net/openapi-client/<spec>`, e.g. `@bunny.net/openapi-client/core`); the older `generated/<spec>.d.ts` paths remain supported.
- Use `clientOptions(config, verbose)` from `packages/cli/src/core/client-options.ts` when creating API clients in command handlers.
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/apps/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import { homedir } from "node:os";
import { basename, isAbsolute, join, resolve } from "node:path";
import type { createMcClient } from "@bunny.net/openapi-client";
import type { components } from "@bunny.net/openapi-client/generated/magic-containers.d.ts";
import prompts from "prompts";
import { resolveRegistryEndpoint } from "../../core/bunny-registry.ts";
import { dockerLogin, imageHostname } from "../../core/docker.ts";
import { ApiError, UserError } from "../../core/errors.ts";
import { logger } from "../../core/logger.ts";
import { spinner } from "../../core/ui.ts";
import { prompts, spinner } from "../../core/ui.ts";

export {
dockerLogin,
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/apps/endpoints/add.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { createMcClient } from "@bunny.net/openapi-client";
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 { spinner } from "../../../core/ui.ts";
import { prompts, spinner } from "../../../core/ui.ts";
import { resolveAppId, resolveContainerId } from "../config.ts";

const COMMAND = "add";
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/apps/link.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { createMcClient } from "@bunny.net/openapi-client";
import type { components } from "@bunny.net/openapi-client/generated/magic-containers.d.ts";
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, saveManifest } from "../../core/manifest.ts";
import { confirm, spinner } from "../../core/ui.ts";
import { confirm, prompts, spinner } from "../../core/ui.ts";
import { APP_MANIFEST, type AppManifest } from "./constants.ts";

type Application = components["schemas"]["Application"];
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/apps/suggestions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { components } from "@bunny.net/openapi-client/generated/magic-containers.d.ts";
import prompts from "prompts";
import { prompts } from "../../core/ui.ts";
import type { ContainerConfig } from "./config.ts";

type EndpointRequest = components["schemas"]["EndpointRequest"];
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/apps/walkthrough.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { existsSync, readFileSync } from "node:fs";
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
import prompts from "prompts";
import { UserError } from "../../core/errors.ts";
import { logger } from "../../core/logger.ts";
import { spinner } from "../../core/ui.ts";
import { prompts, spinner } from "../../core/ui.ts";
import {
composeToConfig,
findComposeFile,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/auth/login.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { randomBytes } from "node:crypto";
import { createCoreClient } from "@bunny.net/openapi-client";
import prompts from "prompts";
import {
profileExists,
resolveConfig,
Expand All @@ -15,6 +14,7 @@ import {
confirm,
isInteractive,
openBrowser,
prompts,
readPassword,
spinner,
} from "../../core/ui.ts";
Expand Down
13 changes: 8 additions & 5 deletions packages/cli/src/commands/db/create.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
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";
import { defineCommand } from "../../core/define-command.ts";
import { UserError } from "../../core/errors.ts";
import { formatKeyValue } from "../../core/format.ts";
import { logger } from "../../core/logger.ts";
import { loadManifest, saveManifest } from "../../core/manifest.ts";
import { confirm, spinner } from "../../core/ui.ts";
import { confirm, prompts, spinner } from "../../core/ui.ts";
import { readEnvValue, writeEnvValue } from "../../utils/env-file.ts";
import { fetchRegionConfig, generateToken } from "./api.ts";
import {
Expand Down Expand Up @@ -365,7 +364,7 @@ export const dbCreateCommand = defineCommand<CreateArgs>({
if (linkArg !== undefined) {
shouldLink = linkArg;
} else if (isInteractive) {
shouldLink = await confirm(linkPrompt, { force: false });
shouldLink = await confirm(linkPrompt, { force: false, optional: true });
} else {
shouldLink = false;
}
Expand All @@ -389,6 +388,7 @@ export const dbCreateCommand = defineCommand<CreateArgs>({
} else if (isInteractive) {
shouldCreateToken = await confirm("Create an auth token?", {
force: false,
optional: true,
});
} else {
shouldCreateToken = false;
Expand Down Expand Up @@ -434,10 +434,13 @@ export const dbCreateCommand = defineCommand<CreateArgs>({
if (existingToken) {
shouldWrite = await confirm(
`${ENV_DATABASE_AUTH_TOKEN} already exists in ${existingToken.envPath} — overwrite?`,
{ force: false },
{ force: false, optional: true },
);
} else {
shouldWrite = await confirm(`Save to .env?`, { force: false });
shouldWrite = await confirm(`Save to .env?`, {
force: false,
optional: true,
});
}
} else {
shouldWrite = false;
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/db/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export const dbDeleteCommand = defineCommand<DeleteArgs>({
if (envUrl && db.url && envUrl.value === db.url) {
const shouldClean = await confirm(
`Remove ${ENV_DATABASE_URL} from ${envUrl.envPath}?`,
{ optional: true },
);

if (shouldClean) {
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/db/link.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { createDbClient } from "@bunny.net/openapi-client";
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 { saveManifest } from "../../core/manifest.ts";
import { spinner } from "../../core/ui.ts";
import { prompts, spinner } from "../../core/ui.ts";
import { fetchAllDatabases, fetchDatabase } from "./api.ts";
import {
ARG_DATABASE_ID,
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/db/quickstart.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { createDbClient } from "@bunny.net/openapi-client";
import chalk from "chalk";
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 { spinner } from "../../core/ui.ts";
import { prompts, spinner } from "../../core/ui.ts";
import { readEnvValue } from "../../utils/env-file.ts";
import { generateToken } from "./api.ts";
import {
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/db/regions/add.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
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";
import { defineCommand } from "../../../core/define-command.ts";
import { formatTable } from "../../../core/format.ts";
import { logger } from "../../../core/logger.ts";
import { spinner } from "../../../core/ui.ts";
import { prompts, spinner } from "../../../core/ui.ts";
import { fetchDatabaseWithRegions, regionNameMap } from "../api.ts";
import { ARG_DATABASE_ID } from "../constants.ts";
import { groupedRegionChoices } from "../region-choices.ts";
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/db/regions/remove.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { createDbClient } from "@bunny.net/openapi-client";
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 { confirm, spinner } from "../../../core/ui.ts";
import { confirm, prompts, spinner } from "../../../core/ui.ts";
import { fetchDatabaseWithRegions, regionNameMap } from "../api.ts";
import { ARG_DATABASE_ID } from "../constants.ts";
import { resolveDbId } from "../resolve-db.ts";
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/db/regions/update.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
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";
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 { prompts, spinner } from "../../../core/ui.ts";
import { fetchDatabaseWithRegions, regionNameMap } from "../api.ts";
import { ARG_DATABASE_ID } from "../constants.ts";
import { groupedRegionChoices } from "../region-choices.ts";
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/db/resolve-db.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { createDbClient } from "@bunny.net/openapi-client";
import type { components } from "@bunny.net/openapi-client/generated/database.d.ts";
import prompts from "prompts";
import { UserError } from "../../core/errors.ts";
import { loadManifest } from "../../core/manifest.ts";
import { spinner } from "../../core/ui.ts";
import { prompts, spinner } from "../../core/ui.ts";
import { readEnvValue } from "../../utils/env-file.ts";
import { fetchAllDatabases } from "./api.ts";
import {
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/db/tokens/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,12 @@ export const dbTokensCreateCommand = defineCommand<{
if (existingToken) {
shouldWrite = await confirm(
`${ENV_DATABASE_AUTH_TOKEN} already exists in ${existingToken.envPath} — overwrite?`,
{ force },
{ force, optional: true },
);
} else {
shouldWrite = await confirm(`Save ${ENV_DATABASE_AUTH_TOKEN} to .env?`, {
force,
optional: true,
});
}

Expand Down
8 changes: 5 additions & 3 deletions packages/cli/src/commands/db/tokens/invalidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export const dbTokensInvalidateCommand = defineCommand<{
if (existingToken) {
const shouldRemove = await confirm(
`Remove ${ENV_DATABASE_AUTH_TOKEN} from ${existingToken.envPath}?`,
{ force },
{ force, optional: true },
);
if (shouldRemove) {
removeEnvValue(ENV_DATABASE_AUTH_TOKEN, existingToken.envPath);
Expand All @@ -175,7 +175,7 @@ export const dbTokensInvalidateCommand = defineCommand<{
// Without --force: prompt the user
const shouldCreate = force
? !!regenerate
: await confirm("Generate a new token?");
: await confirm("Generate a new token?", { optional: true });
if (!shouldCreate) {
logger.warn("All tokens have been invalidated. No valid tokens remain.");
logger.dim(
Expand Down Expand Up @@ -226,7 +226,9 @@ export const dbTokensInvalidateCommand = defineCommand<{
const shouldSave =
saveEnv !== undefined
? saveEnv
: await confirm(`Save ${ENV_DATABASE_AUTH_TOKEN} to .env?`);
: await confirm(`Save ${ENV_DATABASE_AUTH_TOKEN} to .env?`, {
optional: true,
});
if (shouldSave) {
const envPath = existingToken?.envPath;
writeEnvValue(ENV_DATABASE_AUTH_TOKEN, newToken, envPath);
Expand Down
11 changes: 8 additions & 3 deletions packages/cli/src/commands/dns/interactive.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import prompts from "prompts";
import { UserError } from "../../core/errors.ts";
import { logger } from "../../core/logger.ts";
import { loadManifest, saveManifest } from "../../core/manifest.ts";
import type { OutputFormat } from "../../core/types.ts";
import { confirm, isInteractive, spinner } from "../../core/ui.ts";
import { confirm, isInteractive, prompts, spinner } from "../../core/ui.ts";
import {
type CoreClient,
type DnsRecordModel,
Expand All @@ -26,7 +25,12 @@ function writeDnsManifest(id: number, domain: string | undefined): void {

/** Offer to remember a zone picked from the prompt; a no-op if the user declines. */
async function maybeLinkZone(zone: DnsZoneModel): Promise<void> {
if (!(await confirm(`Link this directory to ${zone.Domain}?`))) return;
if (
!(await confirm(`Link this directory to ${zone.Domain}?`, {
optional: true,
}))
)
return;
writeDnsManifest(zone.Id as number, zone.Domain ?? undefined);
}

Expand All @@ -45,6 +49,7 @@ export async function autoLinkDnsZone(zone: {
existing.id &&
!(await confirm(
`This directory is linked to DNS zone ${existing.domain ?? existing.id}. Relink to ${zone.domain ?? zone.id}?`,
{ optional: true },
))
) {
return;
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/dns/record/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import {
createCoreClient,
} from "@bunny.net/openapi-client";
import type { components } from "@bunny.net/openapi-client/generated/core.d.ts";
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 { isInteractive, spinner } from "../../../core/ui.ts";
import { isInteractive, prompts, spinner } from "../../../core/ui.ts";
import type { CoreClient, DnsZoneModel } from "../api.ts";
import { resolveZoneInteractive } from "../interactive.ts";
import {
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/dns/record/import.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { createCoreClient } from "@bunny.net/openapi-client";
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 { spinner } from "../../../core/ui.ts";
import { prompts, spinner } from "../../../core/ui.ts";
import type { CoreClient, DnsZoneModel } from "../api.ts";
import { resolveZoneInteractive } from "../interactive.ts";

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/dns/record/preset.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createCoreClient } from "@bunny.net/openapi-client";
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 { prompts } from "../../../core/ui.ts";
import type { CoreClient, DnsZoneModel } from "../api.ts";
import { resolveZoneInteractive } from "../interactive.ts";
import { type DnsPreset, findPreset, PRESETS } from "./presets.ts";
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/dns/record/update.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { createCoreClient } from "@bunny.net/openapi-client";
import type { components } from "@bunny.net/openapi-client/generated/core.d.ts";
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 { isInteractive, spinner } from "../../../core/ui.ts";
import { isInteractive, prompts, spinner } from "../../../core/ui.ts";
import {
resolveRecordInteractive,
resolveZoneInteractive,
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/dns/record/write.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { components } from "@bunny.net/openapi-client/generated/core.d.ts";
import prompts from "prompts";
import { UserError } from "../../../core/errors.ts";
import { logger } from "../../../core/logger.ts";
import { spinner } from "../../../core/ui.ts";
import { prompts, spinner } from "../../../core/ui.ts";
import type { CoreClient, DnsZoneModel } from "../api.ts";
import { recordName, recordTypeLabel } from "../record-types.ts";

Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/dns/scripts/attach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import {
createCoreClient,
} from "@bunny.net/openapi-client";
import type { components } from "@bunny.net/openapi-client/generated/core.d.ts";
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 { confirm, isInteractive, spinner } from "../../../core/ui.ts";
import { confirm, isInteractive, prompts, spinner } from "../../../core/ui.ts";
import { resolveZoneInteractive } from "../interactive.ts";
import {
type DnsRecordModel,
Expand Down
Loading
Loading