Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions .changeset/dry-crabs-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@bunny.net/cli": patch
"@bunny.net/scriptable-dns-types": patch
---

feat(dns): manage Scriptable DNS scripts (`bunny dns scripts` init/create/deploy/attach/list) with ambient runtime types in `@bunny.net/scriptable-dns-types`
20 changes: 18 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ This is a Bun workspace monorepo with four packages:
- **`@bunny.net/openapi-client`** (`packages/openapi-client/`) — Standalone, type-safe OpenAPI client for bunny.net, generated from OpenAPI specs. Zero CLI dependencies. Publishable to npm.
- **`@bunny.net/app-config`** (`packages/app-config/`) — Shared app configuration schemas (Zod), inferred types, JSON Schema generation, and API conversion functions. Used by the CLI and potentially other tools.
- **`@bunny.net/database-shell`** (`packages/database-shell/`) — Standalone interactive SQL shell for libSQL databases. Framework-agnostic REPL, dot-commands, formatting, masking, and history. Also usable as a standalone CLI (binary: `bsql`).
- **`@bunny.net/scriptable-dns-types`** (`packages/scriptable-dns-types/`) — Ambient TypeScript declarations for the Scriptable DNS runtime globals (`ARecord`, `Monitoring`, `RoutingEngine`, etc.). Types-only, no runtime code: the DNS runtime can't `import`, so these power editor autocomplete and an optional typecheck step. Scaffolded into projects by `bunny dns scripts init`; intended to also feed the dashboard editor. Publishable to npm.
- **`@bunny.net/cli`** (`packages/cli/`) — The CLI. Depends on `@bunny.net/openapi-client`, `@bunny.net/app-config`, and `@bunny.net/database-shell`.

```
Expand Down Expand Up @@ -129,6 +130,11 @@ bunny-cli/
│ │ ├── convert.ts # API ↔ config conversion (apiToConfig, configToAddRequest, configToPatchRequest)
│ │ └── parse-image-ref.ts # Docker image reference parser (parseImageRef)
│ │
│ ├── scriptable-dns-types/ # @bunny.net/scriptable-dns-types package
│ │ ├── package.json # types-only; exports "./index.d.ts" under the "types" condition
│ │ ├── index.d.ts # Ambient globals: ARecord/AaaaRecord/CnameRecord/TxtRecord/PullZoneRecord/Server, Monitoring/GeoDatabase/GeoDistance/RoutingEngine, DnsRequest/DnsQuery/GeoLocation
│ │ └── README.md
│ │
│ ├── database-shell/ # @bunny.net/database-shell package
│ │ ├── package.json # bin: { "bsql": "./src/cli.ts" }
│ │ ├── tsconfig.json
Expand Down Expand Up @@ -273,15 +279,15 @@ bunny-cli/
│ │ │ ├── create.ts # Generate an auth token (read-only/full-access, optional expiry)
│ │ │ └── invalidate.ts # Invalidate all tokens for a database (with confirmation)
│ │ ├── dns/ # Experimental — hidden from help and landing page
│ │ │ ├── index.ts # defineNamespace("dns", ...) — registers the records + zones groups (+ hidden domain aliases)
│ │ │ ├── index.ts # defineNamespace("dns", ...) — registers the records + zones + scripts groups (+ hidden domain aliases)
│ │ │ ├── api.ts # CoreClient type, fetchZones/fetchZone, resolveZone (domain-or-ID → zone)
│ │ │ ├── constants.ts # DNS_MANIFEST (".bunny/dns.json") + DnsManifest type, written by `dns zones link`
│ │ │ ├── interactive.ts # resolveZoneInteractive (arg → .bunny/dns.json manifest → zone picker; offerLink prompts to link a picked zone, skipped under --output json) + resolveRecordInteractive; autoLinkDnsZone (link a zone found in another flow — silent write, confirm before relinking a different zone) reused by scripts custom-domain setup
│ │ │ ├── record-types.ts # Re-exports RECORD_TYPES/recordTypeLabel from core/dns-record-types.ts; adds parseRecordType, recordName, formatRecordValue
│ │ │ ├── record/ # `dns records` — entries within a zone (canonical: records; aliases: record, rec)
│ │ │ │ ├── index.ts # defineNamespace("records", ...)
│ │ │ │ ├── list.ts # List records in a zone (alias: ls)
│ │ │ │ ├── add.ts # Add a record (positional grammar per type, or interactive wizard; --pull-zone/--script)
│ │ │ │ ├── add.ts # Add a record (positional grammar per type, or interactive wizard; --pull-zone/--script; SCRIPT type lists DNS scripts as a picker interactively)
│ │ │ │ ├── update.ts # Update a record (alias: edit; prompts to pick zone+record when omitted)
│ │ │ │ ├── remove.ts # Remove a record (alias: rm; prompts to pick zone+record when omitted)
│ │ │ │ ├── import.ts # Import records from a BIND zone file (prompts for zone/file when omitted)
Expand All @@ -304,6 +310,16 @@ bunny-cli/
│ │ │ ├── index.ts # defineNamespace("logging", ...)
│ │ │ ├── enable.ts # Enable DNS query logging (optional IP anonymization)
│ │ │ └── disable.ts # Disable DNS query logging (with confirmation)
│ │ │ └── scripts/ # `dns scripts` — Scriptable DNS scripts (canonical: scripts; alias: script). Reuses the compute API (EdgeScriptType 0); separate from `bunny scripts` (different runtime, no pull zone)
│ │ │ ├── index.ts # defineNamespace("scripts", ...) — init/create/deploy/attach/list
│ │ │ ├── constants.ts # DNS_SCRIPT_MANIFEST (".bunny/dns-script.json") + DnsScriptManifest; SCRIPT_TYPE_DNS; inline EXAMPLES (empty/geo/closest/weighted/failover/pullzone); TSCONFIG + TYPES_PACKAGE scaffold strings
│ │ │ ├── api.ts # ComputeClient helpers: fetchDnsScripts/fetchDnsScript (type 0), createDnsScript (no pull zone), uploadCode, publishScript
│ │ │ ├── interactive.ts # resolveDnsScriptId (arg → .bunny/dns-script.json → DNS-script picker)
│ │ │ ├── init.ts # Scaffold a project: example chooser, handleQuery.js + tsconfig + package.json (devDep on @bunny.net/scriptable-dns-types) + manifest; optional --deploy
│ │ │ ├── create.ts # Create the remote DNS script (no pull zone), link manifest
│ │ │ ├── deploy.ts # Upload code (single file, no build) + publish; prompts for the file when omitted, --skip-publish to stage
│ │ │ ├── attach.ts # Bridge: add a SCRIPT record on a zone pointing at the script (confirms before the DNS write)
│ │ │ └── list.ts # List DNS scripts (alias: ls)
│ │ ├── registries/
│ │ │ ├── index.ts # Manual CommandModule (not defineNamespace) — default handler runs list
│ │ │ ├── list.ts # List container registries
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Monorepo for the [bunny.net](https://bunny.net) CLI and supporting packages.
| [`packages/database-openapi/`](packages/database-openapi/) | `@bunny.net/database-openapi` | Generate OpenAPI 3.0 specs from a database schema |
| [`packages/database-rest/`](packages/database-rest/) | `@bunny.net/database-rest` | PostgREST-like REST API handler (database-agnostic) |
| [`packages/database-adapter-libsql/`](packages/database-adapter-libsql/) | `@bunny.net/database-adapter-libsql` | Bunny Database adapter for database-rest |
| [`packages/scriptable-dns-types/`](packages/scriptable-dns-types/) | `@bunny.net/scriptable-dns-types` | Ambient TypeScript types for the Scriptable DNS runtime |

See each package's README for usage and API documentation.

Expand Down
22 changes: 14 additions & 8 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/cli/src/commands/dns/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { defineNamespace } from "../../core/define-namespace.ts";
import { dnsRecordNamespace } from "./record/index.ts";
import { dnsScriptsNamespace } from "./scripts/index.ts";
import { dnsZoneHiddenAliases, dnsZoneNamespace } from "./zone/index.ts";

// Hidden from help while experimental, matching the apps and registries namespaces.
export const dnsNamespace = defineNamespace("dns", false, [
dnsRecordNamespace,
dnsZoneNamespace,
dnsScriptsNamespace,
...dnsZoneHiddenAliases,
]);
45 changes: 43 additions & 2 deletions packages/cli/src/commands/dns/record/add.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { createCoreClient } from "@bunny.net/openapi-client";
import {
createComputeClient,
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";
Expand All @@ -15,6 +18,7 @@ import {
recordName,
recordTypeLabel,
} from "../record-types.ts";
import { fetchDnsScripts } from "../scripts/api.ts";

type AddDnsRecordModel = components["schemas"]["AddDnsRecordModel"];
type RecordLinks = Pick<AddDnsRecordModel, "PullZoneId" | "ScriptId">;
Expand Down Expand Up @@ -117,6 +121,7 @@ function required<T>(value: T | undefined, label: string): T {
async function promptRecord(
type: DnsRecordTypes,
name: string,
dnsScripts: Array<{ id: number; name: string }> = [],
): Promise<AddDnsRecordModel> {
const record: AddDnsRecordModel = {
Type: type,
Expand All @@ -134,6 +139,25 @@ async function promptRecord(
}

if (type === RECORD_TYPES.SCRIPT) {
// Offer the account's DNS scripts as a picker; fall back to a manual ID.
if (dnsScripts.length > 0) {
const { id } = await prompts({
type: "select",
name: "id",
message: "DNS script:",
choices: [
...dnsScripts.map((s) => ({
title: `${s.name} (${s.id})`,
value: s.id,
})),
{ title: "Enter a script ID manually", value: -1 },
],
});
if (id !== undefined && id !== -1) {
record.ScriptId = id;
return record;
}
}
const { id } = await prompts({
type: "number",
name: "id",
Expand Down Expand Up @@ -285,7 +309,24 @@ export const dnsAddCommand = defineCommand<AddArgs>({
name = res.name ?? "@";
}

record = await promptRecord(type, name ?? "@");
// For SCRIPT records, offer the account's DNS scripts as a picker.
let dnsScripts: Array<{ id: number; name: string }> = [];
if (type === RECORD_TYPES.SCRIPT) {
const scriptSpin = spinner("Fetching DNS scripts...");
scriptSpin.start();
try {
const computeClient = createComputeClient(
clientOptions(config, verbose),
);
dnsScripts = (await fetchDnsScripts(computeClient))
.filter((s): s is typeof s & { Id: number } => s.Id != null)
.map((s) => ({ id: s.Id, name: s.Name ?? "(unnamed)" }));
} finally {
scriptSpin.stop();
}
}

record = await promptRecord(type, name ?? "@", dnsScripts);

if (args.ttl === undefined) {
const { ttl } = await prompts({
Expand Down
74 changes: 74 additions & 0 deletions packages/cli/src/commands/dns/scripts/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { createComputeClient } from "@bunny.net/openapi-client";
import type { components } from "@bunny.net/openapi-client/generated/compute.d.ts";
import { UserError } from "../../../core/errors.ts";
import { SCRIPT_TYPE_DNS } from "./constants.ts";

export type ComputeClient = ReturnType<typeof createComputeClient>;
export type EdgeScript = components["schemas"]["EdgeScriptModel"];

/** Fetch a single script by ID, throwing a UserError if it doesn't exist. */
export async function fetchDnsScript(
client: ComputeClient,
id: number,
): Promise<EdgeScript> {
const { data } = await client.GET("/compute/script/{id}", {
params: { path: { id } },
});
if (!data) throw new UserError(`DNS script ${id} not found.`);
return data;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

/** Fetch all DNS scripts on the account, sorted by name. */
export async function fetchDnsScripts(
client: ComputeClient,
): Promise<EdgeScript[]> {
const { data } = await client.GET("/compute/script", {
params: { query: { type: [SCRIPT_TYPE_DNS] } },
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
Comment thread
jamie-at-bunny marked this conversation as resolved.
Outdated

return (data?.Items ?? []).sort((a, b) =>
(a.Name ?? "").localeCompare(b.Name ?? ""),
);
}

/** Create a DNS script (no linked pull zone), returning its ID and name. */
export async function createDnsScript(
client: ComputeClient,
name: string,
): Promise<{ id: number; name: string }> {
const { data } = await client.POST("/compute/script", {
body: {
Name: name,
ScriptType: SCRIPT_TYPE_DNS,
CreateLinkedPullZone: false,
},
});

if (!data || data.Id == null) {
throw new UserError("Failed to create DNS script.");
}
return { id: data.Id, name: data.Name ?? name };
}

/** Upload code to a DNS script, creating an unpublished deployment. */
export async function uploadCode(
client: ComputeClient,
id: number,
code: string,
): Promise<void> {
await client.POST("/compute/script/{id}/code", {
params: { path: { id } },
body: { Code: code },
});
}

/** Publish the latest uploaded code as the live release. */
export async function publishScript(
client: ComputeClient,
id: number,
): Promise<void> {
await client.POST("/compute/script/{id}/publish", {
params: { path: { id, uuid: null } },
body: {},
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
Loading