Skip to content
Merged
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
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. `attach [domain] [name] --script <id>`; always confirms (apex gets a louder root-domain warning, lists existing records at the name), --force skips and is required to write non-interactively
│ │ │ ├── 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
90 changes: 90 additions & 0 deletions packages/cli/src/commands/dns/scripts/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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> {
// The {id}/publish path has no {uuid} token (and null is skipped anyway), so this publishes the latest release.
await client.POST("/compute/script/{id}/publish", {
params: { path: { id, uuid: null } },
body: {},
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
Loading
Loading