Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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/link/list) with ambient runtime types in `@bunny.net/scriptable-dns-types`; `dns records add` offers a static or script-computed answer for A/AAAA/CNAME/TXT
21 changes: 19 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). Interactively, A/AAAA/CNAME/TXT offer static vs script-computed (Scriptable DNS) via pickOrCreateDnsScript — pick or create+seed a DNS script and write a SCRIPT record
│ │ │ │ ├── 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,17 @@ 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/link/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)
│ │ │ ├── link.ts # Link this directory to an existing DNS script → .bunny/dns-script.json (positional id, else pick interactively; preserves entry)
│ │ │ └── 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,
]);
56 changes: 54 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,8 @@ import {
recordName,
recordTypeLabel,
} from "../record-types.ts";
import type { AnswerKind } from "../scripts/constants.ts";
import { pickOrCreateDnsScript } from "../scripts/interactive.ts";

type AddDnsRecordModel = components["schemas"]["AddDnsRecordModel"];
type RecordLinks = Pick<AddDnsRecordModel, "PullZoneId" | "ScriptId">;
Expand Down Expand Up @@ -285,7 +290,54 @@ export const dnsAddCommand = defineCommand<AddArgs>({
name = res.name ?? "@";
}

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

// Offer a script-computed answer for types a DNS script can return.
const SCRIPTABLE_ANSWERS = new Set<number>([
RECORD_TYPES.A,
RECORD_TYPES.AAAA,
RECORD_TYPES.CNAME,
RECORD_TYPES.TXT,
]);
let useScript = type === RECORD_TYPES.SCRIPT;
let starterKind: AnswerKind | undefined;
if (!useScript && SCRIPTABLE_ANSWERS.has(type)) {
const { source } = await prompts({
type: "select",
name: "source",
message: "Value source:",
choices: [
{ title: "Static value", value: "static" },
{
title: "Computed by a script (Scriptable DNS)",
value: "script",
},
],
});
if (source === undefined)
throw new UserError("A value source is required.");
if (source === "script") {
useScript = true;
starterKind = recordTypeLabel(type) as AnswerKind;
}
}

if (useScript) {
const computeClient = createComputeClient(
clientOptions(config, verbose),
);
const picked = await pickOrCreateDnsScript(computeClient, {
starterKind,
defaultName: `${zone.Domain ?? "dns"}-script`,
});
record = {
Type: RECORD_TYPES.SCRIPT,
Name: recName === "@" ? "" : recName,
ScriptId: picked.id,
};
} else {
record = await promptRecord(type, recName);
}

if (args.ttl === undefined) {
const { ttl } = await prompts({
Expand Down
89 changes: 89 additions & 0 deletions packages/cli/src/commands/dns/scripts/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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 or is
* not a Scriptable DNS script. Guards callers from treating a CDN or Middleware
* Edge Script as a DNS script (linking it, deploying DNS code, or attaching it).
*/
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.`);
if (data.ScriptType !== SCRIPT_TYPE_DNS) {
throw new UserError(
`Script ${id} is not a Scriptable DNS script.`,
"Pass the ID of a DNS script, or create one with `bunny dns scripts create`.",
);
}
return data;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

/** Fetch all DNS scripts on the account, paginated and sorted by name. */
export async function fetchDnsScripts(
client: ComputeClient,
): Promise<EdgeScript[]> {
const scripts: EdgeScript[] = [];
let page = 1;
for (;;) {
const { data } = await client.GET("/compute/script", {
params: { query: { type: [SCRIPT_TYPE_DNS], page, perPage: 1000 } },
});
scripts.push(...(data?.Items ?? []));
if (!data?.HasMoreItems) break;
page++;
}

return scripts.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