From 876663fa0e65ddbcb310d7e69188e92aa90052b1 Mon Sep 17 00:00:00 2001 From: burstx86 Date: Tue, 2 Jun 2026 01:06:11 +0200 Subject: [PATCH 01/15] added initial pullzone commands --- packages/cli/src/cli.ts | 2 + packages/cli/src/commands/pullzones/clone.ts | 131 ++++++++++++++++++ .../cli/src/commands/pullzones/constants.ts | 8 ++ packages/cli/src/commands/pullzones/create.ts | 108 +++++++++++++++ packages/cli/src/commands/pullzones/delete.ts | 78 +++++++++++ .../cli/src/commands/pullzones/deselect.ts | 23 +++ packages/cli/src/commands/pullzones/index.ts | 112 +++++++++++++++ packages/cli/src/commands/pullzones/list.ts | 64 +++++++++ packages/cli/src/commands/pullzones/purge.ts | 50 +++++++ .../commands/pullzones/resolve-pullzone.ts | 76 ++++++++++ packages/cli/src/commands/pullzones/select.ts | 108 +++++++++++++++ packages/cli/src/commands/pullzones/show.ts | 89 ++++++++++++ 12 files changed, 849 insertions(+) create mode 100644 packages/cli/src/commands/pullzones/clone.ts create mode 100644 packages/cli/src/commands/pullzones/constants.ts create mode 100644 packages/cli/src/commands/pullzones/create.ts create mode 100644 packages/cli/src/commands/pullzones/delete.ts create mode 100644 packages/cli/src/commands/pullzones/deselect.ts create mode 100644 packages/cli/src/commands/pullzones/index.ts create mode 100644 packages/cli/src/commands/pullzones/list.ts create mode 100644 packages/cli/src/commands/pullzones/purge.ts create mode 100644 packages/cli/src/commands/pullzones/resolve-pullzone.ts create mode 100644 packages/cli/src/commands/pullzones/select.ts create mode 100644 packages/cli/src/commands/pullzones/show.ts diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 16e0279..a72e34d 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -10,6 +10,7 @@ import { configNamespace } from "./commands/config/index.ts"; import { dbNamespace } from "./commands/db/index.ts"; import { docsCommand } from "./commands/docs.ts"; import { openCommand } from "./commands/open.ts"; +import { pullzonesNamespace } from "./commands/pullzones/index.ts"; import { registriesNamespace } from "./commands/registries/index.ts"; import { scriptsNamespace } from "./commands/scripts/index.ts"; import { whoamiCommand } from "./commands/whoami.ts"; @@ -23,6 +24,7 @@ const commands: CommandModule[] = [ whoamiCommand, dbNamespace, scriptsNamespace, + pullzonesNamespace, configNamespace, docsCommand, openCommand, diff --git a/packages/cli/src/commands/pullzones/clone.ts b/packages/cli/src/commands/pullzones/clone.ts new file mode 100644 index 0000000..4d36917 --- /dev/null +++ b/packages/cli/src/commands/pullzones/clone.ts @@ -0,0 +1,131 @@ +import { createCoreClient } from "@bunny.net/openapi-client"; +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 { confirm, spinner } from "../../core/ui.ts"; +import { + PULL_ZONE_MANIFEST, + type PullZoneManifest, +} from "./constants.ts"; +import { resolvePullZoneId } from "./resolve-pullzone.ts"; + +interface EdgeRule { + Guid?: string; + ActionType: number; + ActionParameter1?: string; + ActionParameter2?: string; + Description?: string; + Enabled: boolean; + Triggers?: unknown[]; +} + +interface CloneArgs { + source?: string; + target?: string; +} + +export const pullzonesCloneCommand = defineCommand({ + command: "clone ", + describe: "Clone a pull zone.", + examples: [ + ["$0 pullzones clone my-zone my-clone", "Clone by name"], + ["$0 pullzones clone 12345 my-clone", "Clone source by ID"], + ], + + builder: (yargs) => + yargs + .positional("source", { type: "string", describe: "Source pull zone name or ID" }) + .positional("target", { type: "string", describe: "New pull zone name" }), + + handler: async ({ source, target, profile, output, verbose, apiKey }) => { + if (!source || !target) { + throw new UserError("Source and target names are required."); + } + + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + const { id: sourceId } = await resolvePullZoneId(client, source); + + // Fetch full source zone + const fetchSpin = spinner("Fetching source pull zone..."); + fetchSpin.start(); + + const { data } = await client.GET("/pullzone/{id}", { + params: { path: { id: sourceId } }, + }); + + fetchSpin.stop(); + + const zone = data as Record | undefined; + if (!zone) { + throw new UserError(`Source pull zone ${sourceId} not found.`); + } + + // Clone: zero out identity fields, set new name + const cloneBody = { + ...zone, + Id: undefined, + Name: target, + EdgeScriptId: undefined, + MiddlewareScriptId: null, + Hostnames: [], + EdgeRules: undefined, + }; + + const createSpin = spinner("Creating clone..."); + createSpin.start(); + + const { data: newZoneData, error: createError } = await client.POST("/pullzone", { + body: cloneBody as any, + }); + + createSpin.stop(); + + if (createError) { + throw new UserError(`Failed to create clone: ${createError}`); + } + + const newZone = newZoneData as { Id?: number; Name?: string } | undefined; + const newId = newZone?.Id; + if (!newId) { + throw new UserError("Clone created but could not get the new zone ID."); + } + + // Copy edge rules + const sourceRules = (zone.EdgeRules ?? []) as EdgeRule[]; + if (sourceRules.length > 0) { + const ruleSpin = spinner(`Copying ${sourceRules.length} edge rules...`); + ruleSpin.start(); + + for (const rule of sourceRules) { + const { Guid: _, ...ruleBody } = rule; + await client.POST("/pullzone/{pullZoneId}/edgerules/addOrUpdate", { + params: { path: { pullZoneId: newId } }, + body: ruleBody as any, + }); + } + + ruleSpin.stop(); + } + + if (output === "json") { + logger.log(JSON.stringify({ id: newId, name: target, source_id: sourceId })); + return; + } + + logger.success(`Cloned "${source}" → "${target}" (ID: ${newId}).`); + + const shouldSelect = await confirm(`Set "${target}" as the active context?`); + if (shouldSelect) { + saveManifest(PULL_ZONE_MANIFEST, { + id: newId, + name: target, + }); + logger.success(`Selected ${target}.`); + } + }, +}); diff --git a/packages/cli/src/commands/pullzones/constants.ts b/packages/cli/src/commands/pullzones/constants.ts new file mode 100644 index 0000000..d60ccab --- /dev/null +++ b/packages/cli/src/commands/pullzones/constants.ts @@ -0,0 +1,8 @@ +export const ARG_PULL_ZONE_ID = "pull-zone-id"; + +export const PULL_ZONE_MANIFEST = "pullzone.json"; + +export interface PullZoneManifest { + id: number; + name?: string; +} diff --git a/packages/cli/src/commands/pullzones/create.ts b/packages/cli/src/commands/pullzones/create.ts new file mode 100644 index 0000000..83b8604 --- /dev/null +++ b/packages/cli/src/commands/pullzones/create.ts @@ -0,0 +1,108 @@ +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 { saveManifest } from "../../core/manifest.ts"; +import { confirm, spinner } from "../../core/ui.ts"; +import { + PULL_ZONE_MANIFEST, + type PullZoneManifest, +} from "./constants.ts"; + +interface PullZone { + Id: number; + Name?: string | null; +} + +interface CreateArgs { + name?: string; + origin?: string; +} + +export const pullzonesCreateCommand = defineCommand({ + command: "create ", + describe: "Create a new pull zone.", + examples: [ + ["$0 pullzones create my-zone https://origin.example.com", "Create a pull zone"], + ], + + builder: (yargs) => + yargs + .positional("name", { type: "string", describe: "Pull zone name" }) + .positional("origin", { + type: "string", + describe: "Origin URL (https:// is prepended if missing)", + }), + + handler: async ({ name, origin, profile, output, verbose, apiKey }) => { + if (!name || !origin) { + throw new UserError("Name and origin are required."); + } + + const url = origin.match(/^https?:\/\//) ? origin : `https://${origin}`; + + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + // Check if zone already exists + const spin = spinner("Checking for existing zone..."); + spin.start(); + + const { data } = await client.GET("/pullzone"); + const zones = (data ?? []) as PullZone[]; + + spin.stop(); + + const existing = zones.find( + (z) => z.Name?.toLowerCase() === name.toLowerCase(), + ); + if (existing) { + throw new UserError(`Pull zone "${name}" already exists (ID: ${existing.Id}).`); + } + + // Create + const createSpin = spinner("Creating pull zone..."); + createSpin.start(); + + await client.POST("/pullzone", { + body: { Name: name, OriginUrl: url } as any, + }); + + createSpin.stop(); + + // Find the new zone to get its ID + const findSpin = spinner("Fetching new zone..."); + findSpin.start(); + + const { data: updated } = await client.GET("/pullzone"); + const newZone = ((updated ?? []) as PullZone[]).find( + (z) => z.Name?.toLowerCase() === name.toLowerCase(), + ); + + findSpin.stop(); + + if (output === "json") { + logger.log(JSON.stringify({ name, origin: url, id: newZone?.Id ?? null })); + return; + } + + logger.success(`Pull zone "${name}" created.`); + + // Offer to select it + if (newZone) { + const shouldSelect = await confirm( + `Set "${name}" as the active context?`, + ); + if (shouldSelect) { + saveManifest(PULL_ZONE_MANIFEST, { + id: newZone.Id, + name: newZone.Name ?? undefined, + }); + logger.success(`Selected ${name}.`); + } + } + }, +}); diff --git a/packages/cli/src/commands/pullzones/delete.ts b/packages/cli/src/commands/pullzones/delete.ts new file mode 100644 index 0000000..aca3278 --- /dev/null +++ b/packages/cli/src/commands/pullzones/delete.ts @@ -0,0 +1,78 @@ +import { createCoreClient } from "@bunny.net/openapi-client"; +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 { removeManifest } from "../../core/manifest.ts"; +import { confirm, spinner } from "../../core/ui.ts"; +import { + PULL_ZONE_MANIFEST, +} from "./constants.ts"; +import { resolvePullZoneId } from "./resolve-pullzone.ts"; + +interface DeleteArgs { + "name-or-id"?: string; + force?: boolean; +} + +export const pullzonesDeleteCommand = defineCommand({ + command: "delete [name-or-id]", + describe: "Delete a pull zone.", + examples: [ + ["$0 pullzones delete", "Delete selected pull zone"], + ["$0 pullzones delete my-zone", "Delete by name"], + ["$0 pullzones delete 12345", "Delete by ID"], + ["$0 pullzones delete --force", "Skip confirmation"], + ], + + builder: (yargs) => + yargs + .positional("name-or-id", { + type: "string", + describe: "Pull zone name or ID (uses selected one if omitted)", + }) + .option("force", { + alias: "f", + type: "boolean", + default: false, + describe: "Skip confirmation", + }), + + handler: async ({ "name-or-id": nameOrId, force, profile, output, verbose, apiKey }) => { + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + const { id: zoneId, name } = await resolvePullZoneId(client, nameOrId); + + const label = name ?? String(zoneId); + const ok = await confirm(`Delete pull zone "${label}"?`, { force }); + if (!ok) { + logger.log("Delete cancelled."); + return; + } + + const spin = spinner("Deleting pull zone..."); + spin.start(); + + const { error } = await client.DELETE("/pullzone/{id}", { + params: { path: { id: zoneId } }, + }); + + spin.stop(); + + if (error) { + throw new UserError(`Failed to delete pull zone: ${error}`); + } + + // Remove manifest if it pointed at the deleted zone + removeManifest(PULL_ZONE_MANIFEST); + + if (output === "json") { + logger.log(JSON.stringify({ id: zoneId, deleted: true })); + return; + } + + logger.success(`Pull zone "${label}" deleted.`); + }, +}); diff --git a/packages/cli/src/commands/pullzones/deselect.ts b/packages/cli/src/commands/pullzones/deselect.ts new file mode 100644 index 0000000..4d526c0 --- /dev/null +++ b/packages/cli/src/commands/pullzones/deselect.ts @@ -0,0 +1,23 @@ +import { defineCommand } from "../../core/define-command.ts"; +import { logger } from "../../core/logger.ts"; +import { removeManifest } from "../../core/manifest.ts"; +import { PULL_ZONE_MANIFEST } from "./constants.ts"; + +export const pullzonesDeselectCommand = defineCommand({ + command: "deselect", + describe: "Clear the active pull zone context.", + examples: [ + ["$0 pullzones deselect", "Deselect the current pull zone"], + ], + + handler: async ({ output }) => { + removeManifest(PULL_ZONE_MANIFEST); + + if (output === "json") { + logger.log(JSON.stringify({ deselected: true })); + return; + } + + logger.success("Pull zone deselected."); + }, +}); diff --git a/packages/cli/src/commands/pullzones/index.ts b/packages/cli/src/commands/pullzones/index.ts new file mode 100644 index 0000000..d6ed675 --- /dev/null +++ b/packages/cli/src/commands/pullzones/index.ts @@ -0,0 +1,112 @@ +import type { CommandModule } from "yargs"; +import { defineNamespace } from "../../core/define-namespace.ts"; +import { pullzonesCloneCommand } from "./clone.ts"; +import { pullzonesCreateCommand } from "./create.ts"; +import { pullzonesDeleteCommand } from "./delete.ts"; +import { pullzonesDeselectCommand } from "./deselect.ts"; +import { pullzonesListCommand } from "./list.ts"; +import { pullzonesPurgeCommand } from "./purge.ts"; +import { pullzonesSelectCommand } from "./select.ts"; +import { pullzonesShowCommand } from "./show.ts"; + + + +const rulesList: CommandModule = { + command: "list ", + describe: "List edge rules for a pull zone.", + handler: () => {}, +}; + +const rulesAdd: CommandModule = { + command: "add ", + describe: "Add or update an edge rule from a JSON file.", + handler: () => {}, +}; + +const rulesExport: CommandModule = { + command: "export [file]", + describe: "Export an edge rule by name to JSON file or stdout.", + handler: () => {}, +}; + +const rulesCopy: CommandModule = { + command: "copy ", + describe: "Copy all edge rules from one pull zone to another.", + handler: () => {}, +}; + +const rulesDelete: CommandModule = { + command: "delete ", + describe: "Delete an edge rule by GUID.", + handler: () => {}, +}; + +const rulesToggle: CommandModule = { + command: "toggle ", + describe: "Enable or disable an edge rule.", + handler: () => {}, +}; + +const hostnamesList: CommandModule = { + command: "list ", + describe: "List hostnames for a pull zone.", + handler: () => {}, +}; + +const hostnamesAdd: CommandModule = { + command: "add ", + describe: "Add a hostname to a pull zone.", + handler: () => {}, +}; + +const hostnamesRemove: CommandModule = { + command: "remove ", + describe: "Remove a hostname from a pull zone.", + handler: () => {}, +}; + +const hostnamesCert: CommandModule = { + command: "cert ", + describe: "Provision a Let's Encrypt SSL certificate for a hostname.", + handler: () => {}, +}; + +const hostnamesForceSsl: CommandModule = { + command: "forcessl ", + describe: "Enable or disable Force SSL for a hostname.", + handler: () => {}, +}; + +const rulesNamespace = defineNamespace("rules", "Manage pull zone edge rules.", [ + rulesList, + rulesAdd, + rulesExport, + rulesCopy, + rulesDelete, + rulesToggle, +]); + +const hostnamesNamespace = defineNamespace("hostnames", "Manage pull zone hostnames.", [ + hostnamesList, + hostnamesAdd, + hostnamesRemove, + hostnamesCert, + hostnamesForceSsl, +]); + +export const pullzonesNamespace = defineNamespace( + "pullzones", + "Manage pull zones.", + [ + pullzonesListCommand, + pullzonesCreateCommand, + pullzonesCloneCommand, + pullzonesDeleteCommand, + pullzonesSelectCommand, + pullzonesPurgeCommand, + pullzonesShowCommand, + pullzonesDeselectCommand, + rulesNamespace, + hostnamesNamespace, + ], +); diff --git a/packages/cli/src/commands/pullzones/list.ts b/packages/cli/src/commands/pullzones/list.ts new file mode 100644 index 0000000..1158d29 --- /dev/null +++ b/packages/cli/src/commands/pullzones/list.ts @@ -0,0 +1,64 @@ +import { createCoreClient } from "@bunny.net/openapi-client"; +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"; + +interface PullZone { + Id: number; + Name?: string | null; + OriginUrl?: string | null; + Enabled: boolean; + Suspended: boolean; +} + +export const pullzonesListCommand = defineCommand({ + command: "list", + aliases: ["ls"] as const, + describe: "List all pull zones.", + examples: [ + ["$0 pullzones list", "List all pull zones"], + ["$0 pullzones list --output json", "JSON output"], + ], + + handler: async ({ profile, output, verbose, apiKey }) => { + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + const spin = spinner("Fetching pull zones..."); + spin.start(); + + const { data } = await client.GET("/pullzone"); + + spin.stop(); + + const zones = ((data ?? []) as PullZone[]).sort((a: PullZone, b: PullZone) => + (a.Name ?? "").localeCompare(b.Name ?? ""), + ); + + if (output === "json") { + logger.log(JSON.stringify(zones, null, 2)); + return; + } + + if (zones.length === 0) { + logger.info("No pull zones found."); + return; + } + + logger.log( + formatTable( + ["ID", "Name", "Origin", "Status"], + zones.map((zone: PullZone) => [ + String(zone.Id ?? ""), + zone.Name ?? "", + zone.OriginUrl ?? "", + zone.Suspended ? "Suspended" : zone.Enabled ? "Active" : "Disabled", + ]), + output, + ), + ); + }, +}); diff --git a/packages/cli/src/commands/pullzones/purge.ts b/packages/cli/src/commands/pullzones/purge.ts new file mode 100644 index 0000000..e937855 --- /dev/null +++ b/packages/cli/src/commands/pullzones/purge.ts @@ -0,0 +1,50 @@ +import { createCoreClient } from "@bunny.net/openapi-client"; +import { resolveConfig } from "../../config/index.ts"; +import { clientOptions } from "../../core/client-options.ts"; +import { defineCommand } from "../../core/define-command.ts"; +import { logger } from "../../core/logger.ts"; +import { spinner } from "../../core/ui.ts"; +import { resolvePullZoneId } from "./resolve-pullzone.ts"; + +interface PurgeArgs { + "name-or-id"?: string; +} + +export const pullzonesPurgeCommand = defineCommand({ + command: "purge [name-or-id]", + describe: "Purge cached files for a pull zone.", + examples: [ + ["$0 pullzones purge", "Purge cache for selected pull zone"], + ["$0 pullzones purge my-zone", "Purge cache by name"], + ["$0 pullzones purge 12345", "Purge cache by ID"], + ], + + builder: (yargs) => + yargs.positional("name-or-id", { + type: "string", + describe: "Pull zone name or ID (uses selected one if omitted)", + }), + + handler: async ({ "name-or-id": nameOrId, profile, output, verbose, apiKey }) => { + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + const { id: zoneId } = await resolvePullZoneId(client, nameOrId); + + const spin = spinner("Purging cache..."); + spin.start(); + + await client.POST("/pullzone/{id}/purgeCache", { + params: { path: { id: zoneId } }, + }); + + spin.stop(); + + if (output === "json") { + logger.log(JSON.stringify({ id: zoneId, purged: true })); + return; + } + + logger.success(`Cache purged for pull zone ${zoneId}.`); + }, +}); diff --git a/packages/cli/src/commands/pullzones/resolve-pullzone.ts b/packages/cli/src/commands/pullzones/resolve-pullzone.ts new file mode 100644 index 0000000..499f727 --- /dev/null +++ b/packages/cli/src/commands/pullzones/resolve-pullzone.ts @@ -0,0 +1,76 @@ +import type { createCoreClient } from "@bunny.net/openapi-client"; +import { UserError } from "../../core/errors.ts"; +import { loadManifest } from "../../core/manifest.ts"; +import { spinner } from "../../core/ui.ts"; +import { + PULL_ZONE_MANIFEST, + type PullZoneManifest, +} from "./constants.ts"; + +interface PullZone { + Id: number; + Name?: string | null; +} + +export interface ResolvedPullZone { + id: number; + name?: string; + source: "argument" | "manifest"; +} + +/** + * Resolve a pull zone from a name-or-ID, or `.bunny/pullzone.json`. + * + * Tries name lookup first (case-insensitive). If no name matches and the + * value is numeric, falls back to treating it as an ID. No manifest + * fallback when a value is explicitly given. + */ +export async function resolvePullZoneId( + client: ReturnType, + idOrName: string | undefined, +): Promise { + if (idOrName) { + const isNumeric = /^\d+$/.test(idOrName); + + const spin = spinner( + isNumeric + ? `Fetching pull zone ${idOrName}...` + : `Looking up pull zone "${idOrName}"...`, + ); + spin.start(); + + const { data } = await client.GET("/pullzone"); + const zones = (data ?? []) as PullZone[]; + + spin.stop(); + + // Try name match first + const match = zones.find( + (z) => z.Name?.toLowerCase() === idOrName.toLowerCase(), + ); + + if (match) { + return { id: match.Id, name: match.Name ?? undefined, source: "argument" }; + } + + // Fall back to numeric ID + if (isNumeric) { + return { id: Number(idOrName), source: "argument" }; + } + + throw new UserError( + `Pull zone "${idOrName}" not found.`, + "Run `bunny pullzones list` to see available zones.", + ); + } + + const manifest = loadManifest(PULL_ZONE_MANIFEST); + if (manifest.id) { + return { id: manifest.id, name: manifest.name, source: "manifest" }; + } + + throw new UserError( + "No pull zone selected.", + 'Run "bunny pullzones select" or pass a zone name or ID.', + ); +} diff --git a/packages/cli/src/commands/pullzones/select.ts b/packages/cli/src/commands/pullzones/select.ts new file mode 100644 index 0000000..c052bac --- /dev/null +++ b/packages/cli/src/commands/pullzones/select.ts @@ -0,0 +1,108 @@ +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 { saveManifest } from "../../core/manifest.ts"; +import { spinner } from "../../core/ui.ts"; +import { + PULL_ZONE_MANIFEST, + type PullZoneManifest, +} from "./constants.ts"; +import { resolvePullZoneId } from "./resolve-pullzone.ts"; + +interface PullZone { + Id: number; + Name?: string | null; +} + +interface SelectArgs { + "name-or-id"?: string; +} + +export const pullzonesSelectCommand = defineCommand({ + command: "select [name-or-id]", + describe: "Select a pull zone as the active context.", + examples: [ + ["$0 pullzones select", "Interactive selection"], + ["$0 pullzones select my-zone", "Select by name"], + ["$0 pullzones select 12345", "Select by ID"], + ], + + builder: (yargs) => + yargs.positional("name-or-id", { + type: "string", + describe: "Pull zone name or ID", + }), + + handler: async ({ "name-or-id": nameOrId, profile, output, verbose, apiKey }) => { + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + if (nameOrId) { + const { id, name } = await resolvePullZoneId(client, nameOrId); + + saveManifest(PULL_ZONE_MANIFEST, { + id, + name, + }); + + if (output === "json") { + logger.log(JSON.stringify({ id, name })); + return; + } + + logger.success(`Selected ${name ?? id}.`); + return; + } + + const spin = spinner("Fetching pull zones..."); + spin.start(); + + const { data } = await client.GET("/pullzone"); + + spin.stop(); + + const zones = (data ?? []) as PullZone[]; + + if (zones.length === 0) { + throw new UserError( + "No pull zones found.", + 'Run "bunny pullzones create" to create one.', + ); + } + + const sorted = zones.sort((a, b) => + (a.Name ?? "").localeCompare(b.Name ?? ""), + ); + + const { selected } = await prompts({ + type: "select", + name: "selected", + message: "Select a pull zone:", + choices: sorted.map((zone) => ({ + title: zone.Name ?? String(zone.Id), + value: zone, + })), + }); + + if (!selected) { + logger.log("Select cancelled."); + process.exit(1); + } + + saveManifest(PULL_ZONE_MANIFEST, { + id: selected.Id, + name: selected.Name ?? undefined, + }); + + if (output === "json") { + logger.log(JSON.stringify({ id: selected.Id, name: selected.Name })); + return; + } + + logger.success(`Selected ${selected.Name ?? selected.Id}.`); + }, +}); diff --git a/packages/cli/src/commands/pullzones/show.ts b/packages/cli/src/commands/pullzones/show.ts new file mode 100644 index 0000000..a94b68e --- /dev/null +++ b/packages/cli/src/commands/pullzones/show.ts @@ -0,0 +1,89 @@ +import { createCoreClient } from "@bunny.net/openapi-client"; +import { resolveConfig } from "../../config/index.ts"; +import { clientOptions } from "../../core/client-options.ts"; +import { defineCommand } from "../../core/define-command.ts"; +import { formatKeyValue } from "../../core/format.ts"; +import { logger } from "../../core/logger.ts"; +import { resolvePullZoneId } from "./resolve-pullzone.ts"; + +interface PullZone { + Id: number; + Name?: string | null; + OriginUrl?: string | null; + Enabled: boolean; + Suspended: boolean; + Hostnames?: Array<{ Value?: string | null }> | null; + StorageZoneId?: number; + EdgeScriptId?: number; + ZoneSecurityEnabled?: boolean; + ZoneSecurityKey?: string | null; +} + +interface ShowArgs { + "name-or-id"?: string; +} + +export const pullzonesShowCommand = defineCommand({ + command: "show [name-or-id]", + describe: "Show pull zone details.", + examples: [ + ["$0 pullzones show", "Show selected pull zone"], + ["$0 pullzones show my-zone", "Show by name"], + ["$0 pullzones show 12345", "Show by ID"], + ], + + builder: (yargs) => + yargs.positional("name-or-id", { + type: "string", + describe: "Pull zone name or ID (uses selected one if omitted)", + }), + + handler: async ({ "name-or-id": nameOrId, profile, output, verbose, apiKey }) => { + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + const { id: zoneId } = await resolvePullZoneId(client, nameOrId); + + const { data } = await client.GET("/pullzone/{id}", { + params: { path: { id: zoneId } }, + }); + + const zone = data as PullZone | undefined; + if (!zone) { + logger.error(`Pull zone ${zoneId} not found.`); + return; + } + + if (output === "json") { + logger.log(JSON.stringify(zone, null, 2)); + return; + } + + const hostnames = zone.Hostnames + ?.map((h) => h.Value) + .filter(Boolean) + .join(", ") ?? "none"; + + const status = zone.Suspended + ? "Suspended" + : zone.Enabled + ? "Active" + : "Disabled"; + + logger.log( + formatKeyValue( + [ + { key: "ID", value: String(zone.Id) }, + { key: "Name", value: zone.Name ?? "" }, + { key: "Origin", value: zone.OriginUrl ?? "" }, + { key: "Status", value: status }, + { key: "Hostnames", value: hostnames }, + { key: "Storage Zone ID", value: String(zone.StorageZoneId ?? "") }, + { key: "Edge Script ID", value: String(zone.EdgeScriptId ?? "") }, + { key: "Security", value: zone.ZoneSecurityEnabled ? "Enabled" : "Disabled" }, + ], + output, + ), + ); + }, +}); From 337171d19f3c7271a3a35983bf3acc29f9793fa2 Mon Sep 17 00:00:00 2001 From: burstx86 Date: Tue, 2 Jun 2026 01:08:22 +0200 Subject: [PATCH 02/15] changed pullzone to pz --- packages/cli/src/cli.ts | 4 +- packages/cli/src/commands/pullzones/clone.ts | 131 ------------------ .../cli/src/commands/pullzones/constants.ts | 8 -- packages/cli/src/commands/pullzones/create.ts | 108 --------------- packages/cli/src/commands/pullzones/delete.ts | 78 ----------- .../cli/src/commands/pullzones/deselect.ts | 23 --- packages/cli/src/commands/pullzones/index.ts | 112 --------------- packages/cli/src/commands/pullzones/list.ts | 64 --------- packages/cli/src/commands/pullzones/purge.ts | 50 ------- .../commands/pullzones/resolve-pullzone.ts | 76 ---------- packages/cli/src/commands/pullzones/select.ts | 108 --------------- packages/cli/src/commands/pullzones/show.ts | 89 ------------ 12 files changed, 2 insertions(+), 849 deletions(-) delete mode 100644 packages/cli/src/commands/pullzones/clone.ts delete mode 100644 packages/cli/src/commands/pullzones/constants.ts delete mode 100644 packages/cli/src/commands/pullzones/create.ts delete mode 100644 packages/cli/src/commands/pullzones/delete.ts delete mode 100644 packages/cli/src/commands/pullzones/deselect.ts delete mode 100644 packages/cli/src/commands/pullzones/index.ts delete mode 100644 packages/cli/src/commands/pullzones/list.ts delete mode 100644 packages/cli/src/commands/pullzones/purge.ts delete mode 100644 packages/cli/src/commands/pullzones/resolve-pullzone.ts delete mode 100644 packages/cli/src/commands/pullzones/select.ts delete mode 100644 packages/cli/src/commands/pullzones/show.ts diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index a72e34d..7a61722 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -10,7 +10,7 @@ import { configNamespace } from "./commands/config/index.ts"; import { dbNamespace } from "./commands/db/index.ts"; import { docsCommand } from "./commands/docs.ts"; import { openCommand } from "./commands/open.ts"; -import { pullzonesNamespace } from "./commands/pullzones/index.ts"; +import { pzNamespace } from "./commands/pz/index.ts"; import { registriesNamespace } from "./commands/registries/index.ts"; import { scriptsNamespace } from "./commands/scripts/index.ts"; import { whoamiCommand } from "./commands/whoami.ts"; @@ -24,7 +24,7 @@ const commands: CommandModule[] = [ whoamiCommand, dbNamespace, scriptsNamespace, - pullzonesNamespace, + pzNamespace, configNamespace, docsCommand, openCommand, diff --git a/packages/cli/src/commands/pullzones/clone.ts b/packages/cli/src/commands/pullzones/clone.ts deleted file mode 100644 index 4d36917..0000000 --- a/packages/cli/src/commands/pullzones/clone.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { createCoreClient } from "@bunny.net/openapi-client"; -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 { confirm, spinner } from "../../core/ui.ts"; -import { - PULL_ZONE_MANIFEST, - type PullZoneManifest, -} from "./constants.ts"; -import { resolvePullZoneId } from "./resolve-pullzone.ts"; - -interface EdgeRule { - Guid?: string; - ActionType: number; - ActionParameter1?: string; - ActionParameter2?: string; - Description?: string; - Enabled: boolean; - Triggers?: unknown[]; -} - -interface CloneArgs { - source?: string; - target?: string; -} - -export const pullzonesCloneCommand = defineCommand({ - command: "clone ", - describe: "Clone a pull zone.", - examples: [ - ["$0 pullzones clone my-zone my-clone", "Clone by name"], - ["$0 pullzones clone 12345 my-clone", "Clone source by ID"], - ], - - builder: (yargs) => - yargs - .positional("source", { type: "string", describe: "Source pull zone name or ID" }) - .positional("target", { type: "string", describe: "New pull zone name" }), - - handler: async ({ source, target, profile, output, verbose, apiKey }) => { - if (!source || !target) { - throw new UserError("Source and target names are required."); - } - - const config = resolveConfig(profile, apiKey, verbose); - const client = createCoreClient(clientOptions(config, verbose)); - - const { id: sourceId } = await resolvePullZoneId(client, source); - - // Fetch full source zone - const fetchSpin = spinner("Fetching source pull zone..."); - fetchSpin.start(); - - const { data } = await client.GET("/pullzone/{id}", { - params: { path: { id: sourceId } }, - }); - - fetchSpin.stop(); - - const zone = data as Record | undefined; - if (!zone) { - throw new UserError(`Source pull zone ${sourceId} not found.`); - } - - // Clone: zero out identity fields, set new name - const cloneBody = { - ...zone, - Id: undefined, - Name: target, - EdgeScriptId: undefined, - MiddlewareScriptId: null, - Hostnames: [], - EdgeRules: undefined, - }; - - const createSpin = spinner("Creating clone..."); - createSpin.start(); - - const { data: newZoneData, error: createError } = await client.POST("/pullzone", { - body: cloneBody as any, - }); - - createSpin.stop(); - - if (createError) { - throw new UserError(`Failed to create clone: ${createError}`); - } - - const newZone = newZoneData as { Id?: number; Name?: string } | undefined; - const newId = newZone?.Id; - if (!newId) { - throw new UserError("Clone created but could not get the new zone ID."); - } - - // Copy edge rules - const sourceRules = (zone.EdgeRules ?? []) as EdgeRule[]; - if (sourceRules.length > 0) { - const ruleSpin = spinner(`Copying ${sourceRules.length} edge rules...`); - ruleSpin.start(); - - for (const rule of sourceRules) { - const { Guid: _, ...ruleBody } = rule; - await client.POST("/pullzone/{pullZoneId}/edgerules/addOrUpdate", { - params: { path: { pullZoneId: newId } }, - body: ruleBody as any, - }); - } - - ruleSpin.stop(); - } - - if (output === "json") { - logger.log(JSON.stringify({ id: newId, name: target, source_id: sourceId })); - return; - } - - logger.success(`Cloned "${source}" → "${target}" (ID: ${newId}).`); - - const shouldSelect = await confirm(`Set "${target}" as the active context?`); - if (shouldSelect) { - saveManifest(PULL_ZONE_MANIFEST, { - id: newId, - name: target, - }); - logger.success(`Selected ${target}.`); - } - }, -}); diff --git a/packages/cli/src/commands/pullzones/constants.ts b/packages/cli/src/commands/pullzones/constants.ts deleted file mode 100644 index d60ccab..0000000 --- a/packages/cli/src/commands/pullzones/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const ARG_PULL_ZONE_ID = "pull-zone-id"; - -export const PULL_ZONE_MANIFEST = "pullzone.json"; - -export interface PullZoneManifest { - id: number; - name?: string; -} diff --git a/packages/cli/src/commands/pullzones/create.ts b/packages/cli/src/commands/pullzones/create.ts deleted file mode 100644 index 83b8604..0000000 --- a/packages/cli/src/commands/pullzones/create.ts +++ /dev/null @@ -1,108 +0,0 @@ -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 { saveManifest } from "../../core/manifest.ts"; -import { confirm, spinner } from "../../core/ui.ts"; -import { - PULL_ZONE_MANIFEST, - type PullZoneManifest, -} from "./constants.ts"; - -interface PullZone { - Id: number; - Name?: string | null; -} - -interface CreateArgs { - name?: string; - origin?: string; -} - -export const pullzonesCreateCommand = defineCommand({ - command: "create ", - describe: "Create a new pull zone.", - examples: [ - ["$0 pullzones create my-zone https://origin.example.com", "Create a pull zone"], - ], - - builder: (yargs) => - yargs - .positional("name", { type: "string", describe: "Pull zone name" }) - .positional("origin", { - type: "string", - describe: "Origin URL (https:// is prepended if missing)", - }), - - handler: async ({ name, origin, profile, output, verbose, apiKey }) => { - if (!name || !origin) { - throw new UserError("Name and origin are required."); - } - - const url = origin.match(/^https?:\/\//) ? origin : `https://${origin}`; - - const config = resolveConfig(profile, apiKey, verbose); - const client = createCoreClient(clientOptions(config, verbose)); - - // Check if zone already exists - const spin = spinner("Checking for existing zone..."); - spin.start(); - - const { data } = await client.GET("/pullzone"); - const zones = (data ?? []) as PullZone[]; - - spin.stop(); - - const existing = zones.find( - (z) => z.Name?.toLowerCase() === name.toLowerCase(), - ); - if (existing) { - throw new UserError(`Pull zone "${name}" already exists (ID: ${existing.Id}).`); - } - - // Create - const createSpin = spinner("Creating pull zone..."); - createSpin.start(); - - await client.POST("/pullzone", { - body: { Name: name, OriginUrl: url } as any, - }); - - createSpin.stop(); - - // Find the new zone to get its ID - const findSpin = spinner("Fetching new zone..."); - findSpin.start(); - - const { data: updated } = await client.GET("/pullzone"); - const newZone = ((updated ?? []) as PullZone[]).find( - (z) => z.Name?.toLowerCase() === name.toLowerCase(), - ); - - findSpin.stop(); - - if (output === "json") { - logger.log(JSON.stringify({ name, origin: url, id: newZone?.Id ?? null })); - return; - } - - logger.success(`Pull zone "${name}" created.`); - - // Offer to select it - if (newZone) { - const shouldSelect = await confirm( - `Set "${name}" as the active context?`, - ); - if (shouldSelect) { - saveManifest(PULL_ZONE_MANIFEST, { - id: newZone.Id, - name: newZone.Name ?? undefined, - }); - logger.success(`Selected ${name}.`); - } - } - }, -}); diff --git a/packages/cli/src/commands/pullzones/delete.ts b/packages/cli/src/commands/pullzones/delete.ts deleted file mode 100644 index aca3278..0000000 --- a/packages/cli/src/commands/pullzones/delete.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { createCoreClient } from "@bunny.net/openapi-client"; -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 { removeManifest } from "../../core/manifest.ts"; -import { confirm, spinner } from "../../core/ui.ts"; -import { - PULL_ZONE_MANIFEST, -} from "./constants.ts"; -import { resolvePullZoneId } from "./resolve-pullzone.ts"; - -interface DeleteArgs { - "name-or-id"?: string; - force?: boolean; -} - -export const pullzonesDeleteCommand = defineCommand({ - command: "delete [name-or-id]", - describe: "Delete a pull zone.", - examples: [ - ["$0 pullzones delete", "Delete selected pull zone"], - ["$0 pullzones delete my-zone", "Delete by name"], - ["$0 pullzones delete 12345", "Delete by ID"], - ["$0 pullzones delete --force", "Skip confirmation"], - ], - - builder: (yargs) => - yargs - .positional("name-or-id", { - type: "string", - describe: "Pull zone name or ID (uses selected one if omitted)", - }) - .option("force", { - alias: "f", - type: "boolean", - default: false, - describe: "Skip confirmation", - }), - - handler: async ({ "name-or-id": nameOrId, force, profile, output, verbose, apiKey }) => { - const config = resolveConfig(profile, apiKey, verbose); - const client = createCoreClient(clientOptions(config, verbose)); - - const { id: zoneId, name } = await resolvePullZoneId(client, nameOrId); - - const label = name ?? String(zoneId); - const ok = await confirm(`Delete pull zone "${label}"?`, { force }); - if (!ok) { - logger.log("Delete cancelled."); - return; - } - - const spin = spinner("Deleting pull zone..."); - spin.start(); - - const { error } = await client.DELETE("/pullzone/{id}", { - params: { path: { id: zoneId } }, - }); - - spin.stop(); - - if (error) { - throw new UserError(`Failed to delete pull zone: ${error}`); - } - - // Remove manifest if it pointed at the deleted zone - removeManifest(PULL_ZONE_MANIFEST); - - if (output === "json") { - logger.log(JSON.stringify({ id: zoneId, deleted: true })); - return; - } - - logger.success(`Pull zone "${label}" deleted.`); - }, -}); diff --git a/packages/cli/src/commands/pullzones/deselect.ts b/packages/cli/src/commands/pullzones/deselect.ts deleted file mode 100644 index 4d526c0..0000000 --- a/packages/cli/src/commands/pullzones/deselect.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineCommand } from "../../core/define-command.ts"; -import { logger } from "../../core/logger.ts"; -import { removeManifest } from "../../core/manifest.ts"; -import { PULL_ZONE_MANIFEST } from "./constants.ts"; - -export const pullzonesDeselectCommand = defineCommand({ - command: "deselect", - describe: "Clear the active pull zone context.", - examples: [ - ["$0 pullzones deselect", "Deselect the current pull zone"], - ], - - handler: async ({ output }) => { - removeManifest(PULL_ZONE_MANIFEST); - - if (output === "json") { - logger.log(JSON.stringify({ deselected: true })); - return; - } - - logger.success("Pull zone deselected."); - }, -}); diff --git a/packages/cli/src/commands/pullzones/index.ts b/packages/cli/src/commands/pullzones/index.ts deleted file mode 100644 index d6ed675..0000000 --- a/packages/cli/src/commands/pullzones/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { CommandModule } from "yargs"; -import { defineNamespace } from "../../core/define-namespace.ts"; -import { pullzonesCloneCommand } from "./clone.ts"; -import { pullzonesCreateCommand } from "./create.ts"; -import { pullzonesDeleteCommand } from "./delete.ts"; -import { pullzonesDeselectCommand } from "./deselect.ts"; -import { pullzonesListCommand } from "./list.ts"; -import { pullzonesPurgeCommand } from "./purge.ts"; -import { pullzonesSelectCommand } from "./select.ts"; -import { pullzonesShowCommand } from "./show.ts"; - - - -const rulesList: CommandModule = { - command: "list ", - describe: "List edge rules for a pull zone.", - handler: () => {}, -}; - -const rulesAdd: CommandModule = { - command: "add ", - describe: "Add or update an edge rule from a JSON file.", - handler: () => {}, -}; - -const rulesExport: CommandModule = { - command: "export [file]", - describe: "Export an edge rule by name to JSON file or stdout.", - handler: () => {}, -}; - -const rulesCopy: CommandModule = { - command: "copy ", - describe: "Copy all edge rules from one pull zone to another.", - handler: () => {}, -}; - -const rulesDelete: CommandModule = { - command: "delete ", - describe: "Delete an edge rule by GUID.", - handler: () => {}, -}; - -const rulesToggle: CommandModule = { - command: "toggle ", - describe: "Enable or disable an edge rule.", - handler: () => {}, -}; - -const hostnamesList: CommandModule = { - command: "list ", - describe: "List hostnames for a pull zone.", - handler: () => {}, -}; - -const hostnamesAdd: CommandModule = { - command: "add ", - describe: "Add a hostname to a pull zone.", - handler: () => {}, -}; - -const hostnamesRemove: CommandModule = { - command: "remove ", - describe: "Remove a hostname from a pull zone.", - handler: () => {}, -}; - -const hostnamesCert: CommandModule = { - command: "cert ", - describe: "Provision a Let's Encrypt SSL certificate for a hostname.", - handler: () => {}, -}; - -const hostnamesForceSsl: CommandModule = { - command: "forcessl ", - describe: "Enable or disable Force SSL for a hostname.", - handler: () => {}, -}; - -const rulesNamespace = defineNamespace("rules", "Manage pull zone edge rules.", [ - rulesList, - rulesAdd, - rulesExport, - rulesCopy, - rulesDelete, - rulesToggle, -]); - -const hostnamesNamespace = defineNamespace("hostnames", "Manage pull zone hostnames.", [ - hostnamesList, - hostnamesAdd, - hostnamesRemove, - hostnamesCert, - hostnamesForceSsl, -]); - -export const pullzonesNamespace = defineNamespace( - "pullzones", - "Manage pull zones.", - [ - pullzonesListCommand, - pullzonesCreateCommand, - pullzonesCloneCommand, - pullzonesDeleteCommand, - pullzonesSelectCommand, - pullzonesPurgeCommand, - pullzonesShowCommand, - pullzonesDeselectCommand, - rulesNamespace, - hostnamesNamespace, - ], -); diff --git a/packages/cli/src/commands/pullzones/list.ts b/packages/cli/src/commands/pullzones/list.ts deleted file mode 100644 index 1158d29..0000000 --- a/packages/cli/src/commands/pullzones/list.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { createCoreClient } from "@bunny.net/openapi-client"; -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"; - -interface PullZone { - Id: number; - Name?: string | null; - OriginUrl?: string | null; - Enabled: boolean; - Suspended: boolean; -} - -export const pullzonesListCommand = defineCommand({ - command: "list", - aliases: ["ls"] as const, - describe: "List all pull zones.", - examples: [ - ["$0 pullzones list", "List all pull zones"], - ["$0 pullzones list --output json", "JSON output"], - ], - - handler: async ({ profile, output, verbose, apiKey }) => { - const config = resolveConfig(profile, apiKey, verbose); - const client = createCoreClient(clientOptions(config, verbose)); - - const spin = spinner("Fetching pull zones..."); - spin.start(); - - const { data } = await client.GET("/pullzone"); - - spin.stop(); - - const zones = ((data ?? []) as PullZone[]).sort((a: PullZone, b: PullZone) => - (a.Name ?? "").localeCompare(b.Name ?? ""), - ); - - if (output === "json") { - logger.log(JSON.stringify(zones, null, 2)); - return; - } - - if (zones.length === 0) { - logger.info("No pull zones found."); - return; - } - - logger.log( - formatTable( - ["ID", "Name", "Origin", "Status"], - zones.map((zone: PullZone) => [ - String(zone.Id ?? ""), - zone.Name ?? "", - zone.OriginUrl ?? "", - zone.Suspended ? "Suspended" : zone.Enabled ? "Active" : "Disabled", - ]), - output, - ), - ); - }, -}); diff --git a/packages/cli/src/commands/pullzones/purge.ts b/packages/cli/src/commands/pullzones/purge.ts deleted file mode 100644 index e937855..0000000 --- a/packages/cli/src/commands/pullzones/purge.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createCoreClient } from "@bunny.net/openapi-client"; -import { resolveConfig } from "../../config/index.ts"; -import { clientOptions } from "../../core/client-options.ts"; -import { defineCommand } from "../../core/define-command.ts"; -import { logger } from "../../core/logger.ts"; -import { spinner } from "../../core/ui.ts"; -import { resolvePullZoneId } from "./resolve-pullzone.ts"; - -interface PurgeArgs { - "name-or-id"?: string; -} - -export const pullzonesPurgeCommand = defineCommand({ - command: "purge [name-or-id]", - describe: "Purge cached files for a pull zone.", - examples: [ - ["$0 pullzones purge", "Purge cache for selected pull zone"], - ["$0 pullzones purge my-zone", "Purge cache by name"], - ["$0 pullzones purge 12345", "Purge cache by ID"], - ], - - builder: (yargs) => - yargs.positional("name-or-id", { - type: "string", - describe: "Pull zone name or ID (uses selected one if omitted)", - }), - - handler: async ({ "name-or-id": nameOrId, profile, output, verbose, apiKey }) => { - const config = resolveConfig(profile, apiKey, verbose); - const client = createCoreClient(clientOptions(config, verbose)); - - const { id: zoneId } = await resolvePullZoneId(client, nameOrId); - - const spin = spinner("Purging cache..."); - spin.start(); - - await client.POST("/pullzone/{id}/purgeCache", { - params: { path: { id: zoneId } }, - }); - - spin.stop(); - - if (output === "json") { - logger.log(JSON.stringify({ id: zoneId, purged: true })); - return; - } - - logger.success(`Cache purged for pull zone ${zoneId}.`); - }, -}); diff --git a/packages/cli/src/commands/pullzones/resolve-pullzone.ts b/packages/cli/src/commands/pullzones/resolve-pullzone.ts deleted file mode 100644 index 499f727..0000000 --- a/packages/cli/src/commands/pullzones/resolve-pullzone.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { createCoreClient } from "@bunny.net/openapi-client"; -import { UserError } from "../../core/errors.ts"; -import { loadManifest } from "../../core/manifest.ts"; -import { spinner } from "../../core/ui.ts"; -import { - PULL_ZONE_MANIFEST, - type PullZoneManifest, -} from "./constants.ts"; - -interface PullZone { - Id: number; - Name?: string | null; -} - -export interface ResolvedPullZone { - id: number; - name?: string; - source: "argument" | "manifest"; -} - -/** - * Resolve a pull zone from a name-or-ID, or `.bunny/pullzone.json`. - * - * Tries name lookup first (case-insensitive). If no name matches and the - * value is numeric, falls back to treating it as an ID. No manifest - * fallback when a value is explicitly given. - */ -export async function resolvePullZoneId( - client: ReturnType, - idOrName: string | undefined, -): Promise { - if (idOrName) { - const isNumeric = /^\d+$/.test(idOrName); - - const spin = spinner( - isNumeric - ? `Fetching pull zone ${idOrName}...` - : `Looking up pull zone "${idOrName}"...`, - ); - spin.start(); - - const { data } = await client.GET("/pullzone"); - const zones = (data ?? []) as PullZone[]; - - spin.stop(); - - // Try name match first - const match = zones.find( - (z) => z.Name?.toLowerCase() === idOrName.toLowerCase(), - ); - - if (match) { - return { id: match.Id, name: match.Name ?? undefined, source: "argument" }; - } - - // Fall back to numeric ID - if (isNumeric) { - return { id: Number(idOrName), source: "argument" }; - } - - throw new UserError( - `Pull zone "${idOrName}" not found.`, - "Run `bunny pullzones list` to see available zones.", - ); - } - - const manifest = loadManifest(PULL_ZONE_MANIFEST); - if (manifest.id) { - return { id: manifest.id, name: manifest.name, source: "manifest" }; - } - - throw new UserError( - "No pull zone selected.", - 'Run "bunny pullzones select" or pass a zone name or ID.', - ); -} diff --git a/packages/cli/src/commands/pullzones/select.ts b/packages/cli/src/commands/pullzones/select.ts deleted file mode 100644 index c052bac..0000000 --- a/packages/cli/src/commands/pullzones/select.ts +++ /dev/null @@ -1,108 +0,0 @@ -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 { saveManifest } from "../../core/manifest.ts"; -import { spinner } from "../../core/ui.ts"; -import { - PULL_ZONE_MANIFEST, - type PullZoneManifest, -} from "./constants.ts"; -import { resolvePullZoneId } from "./resolve-pullzone.ts"; - -interface PullZone { - Id: number; - Name?: string | null; -} - -interface SelectArgs { - "name-or-id"?: string; -} - -export const pullzonesSelectCommand = defineCommand({ - command: "select [name-or-id]", - describe: "Select a pull zone as the active context.", - examples: [ - ["$0 pullzones select", "Interactive selection"], - ["$0 pullzones select my-zone", "Select by name"], - ["$0 pullzones select 12345", "Select by ID"], - ], - - builder: (yargs) => - yargs.positional("name-or-id", { - type: "string", - describe: "Pull zone name or ID", - }), - - handler: async ({ "name-or-id": nameOrId, profile, output, verbose, apiKey }) => { - const config = resolveConfig(profile, apiKey, verbose); - const client = createCoreClient(clientOptions(config, verbose)); - - if (nameOrId) { - const { id, name } = await resolvePullZoneId(client, nameOrId); - - saveManifest(PULL_ZONE_MANIFEST, { - id, - name, - }); - - if (output === "json") { - logger.log(JSON.stringify({ id, name })); - return; - } - - logger.success(`Selected ${name ?? id}.`); - return; - } - - const spin = spinner("Fetching pull zones..."); - spin.start(); - - const { data } = await client.GET("/pullzone"); - - spin.stop(); - - const zones = (data ?? []) as PullZone[]; - - if (zones.length === 0) { - throw new UserError( - "No pull zones found.", - 'Run "bunny pullzones create" to create one.', - ); - } - - const sorted = zones.sort((a, b) => - (a.Name ?? "").localeCompare(b.Name ?? ""), - ); - - const { selected } = await prompts({ - type: "select", - name: "selected", - message: "Select a pull zone:", - choices: sorted.map((zone) => ({ - title: zone.Name ?? String(zone.Id), - value: zone, - })), - }); - - if (!selected) { - logger.log("Select cancelled."); - process.exit(1); - } - - saveManifest(PULL_ZONE_MANIFEST, { - id: selected.Id, - name: selected.Name ?? undefined, - }); - - if (output === "json") { - logger.log(JSON.stringify({ id: selected.Id, name: selected.Name })); - return; - } - - logger.success(`Selected ${selected.Name ?? selected.Id}.`); - }, -}); diff --git a/packages/cli/src/commands/pullzones/show.ts b/packages/cli/src/commands/pullzones/show.ts deleted file mode 100644 index a94b68e..0000000 --- a/packages/cli/src/commands/pullzones/show.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { createCoreClient } from "@bunny.net/openapi-client"; -import { resolveConfig } from "../../config/index.ts"; -import { clientOptions } from "../../core/client-options.ts"; -import { defineCommand } from "../../core/define-command.ts"; -import { formatKeyValue } from "../../core/format.ts"; -import { logger } from "../../core/logger.ts"; -import { resolvePullZoneId } from "./resolve-pullzone.ts"; - -interface PullZone { - Id: number; - Name?: string | null; - OriginUrl?: string | null; - Enabled: boolean; - Suspended: boolean; - Hostnames?: Array<{ Value?: string | null }> | null; - StorageZoneId?: number; - EdgeScriptId?: number; - ZoneSecurityEnabled?: boolean; - ZoneSecurityKey?: string | null; -} - -interface ShowArgs { - "name-or-id"?: string; -} - -export const pullzonesShowCommand = defineCommand({ - command: "show [name-or-id]", - describe: "Show pull zone details.", - examples: [ - ["$0 pullzones show", "Show selected pull zone"], - ["$0 pullzones show my-zone", "Show by name"], - ["$0 pullzones show 12345", "Show by ID"], - ], - - builder: (yargs) => - yargs.positional("name-or-id", { - type: "string", - describe: "Pull zone name or ID (uses selected one if omitted)", - }), - - handler: async ({ "name-or-id": nameOrId, profile, output, verbose, apiKey }) => { - const config = resolveConfig(profile, apiKey, verbose); - const client = createCoreClient(clientOptions(config, verbose)); - - const { id: zoneId } = await resolvePullZoneId(client, nameOrId); - - const { data } = await client.GET("/pullzone/{id}", { - params: { path: { id: zoneId } }, - }); - - const zone = data as PullZone | undefined; - if (!zone) { - logger.error(`Pull zone ${zoneId} not found.`); - return; - } - - if (output === "json") { - logger.log(JSON.stringify(zone, null, 2)); - return; - } - - const hostnames = zone.Hostnames - ?.map((h) => h.Value) - .filter(Boolean) - .join(", ") ?? "none"; - - const status = zone.Suspended - ? "Suspended" - : zone.Enabled - ? "Active" - : "Disabled"; - - logger.log( - formatKeyValue( - [ - { key: "ID", value: String(zone.Id) }, - { key: "Name", value: zone.Name ?? "" }, - { key: "Origin", value: zone.OriginUrl ?? "" }, - { key: "Status", value: status }, - { key: "Hostnames", value: hostnames }, - { key: "Storage Zone ID", value: String(zone.StorageZoneId ?? "") }, - { key: "Edge Script ID", value: String(zone.EdgeScriptId ?? "") }, - { key: "Security", value: zone.ZoneSecurityEnabled ? "Enabled" : "Disabled" }, - ], - output, - ), - ); - }, -}); From 54b9cbc72088152bb922a488ca014f5343e3b33f Mon Sep 17 00:00:00 2001 From: burstx86 Date: Tue, 2 Jun 2026 01:10:39 +0200 Subject: [PATCH 03/15] changed pullzone to pz --- packages/cli/src/commands/pz/clone.ts | 131 ++++++++++++++++++ packages/cli/src/commands/pz/constants.ts | 8 ++ packages/cli/src/commands/pz/create.ts | 108 +++++++++++++++ packages/cli/src/commands/pz/delete.ts | 78 +++++++++++ packages/cli/src/commands/pz/deselect.ts | 23 +++ packages/cli/src/commands/pz/index.ts | 112 +++++++++++++++ packages/cli/src/commands/pz/list.ts | 64 +++++++++ packages/cli/src/commands/pz/purge.ts | 50 +++++++ .../cli/src/commands/pz/resolve-pullzone.ts | 76 ++++++++++ packages/cli/src/commands/pz/select.ts | 108 +++++++++++++++ packages/cli/src/commands/pz/show.ts | 89 ++++++++++++ 11 files changed, 847 insertions(+) create mode 100644 packages/cli/src/commands/pz/clone.ts create mode 100644 packages/cli/src/commands/pz/constants.ts create mode 100644 packages/cli/src/commands/pz/create.ts create mode 100644 packages/cli/src/commands/pz/delete.ts create mode 100644 packages/cli/src/commands/pz/deselect.ts create mode 100644 packages/cli/src/commands/pz/index.ts create mode 100644 packages/cli/src/commands/pz/list.ts create mode 100644 packages/cli/src/commands/pz/purge.ts create mode 100644 packages/cli/src/commands/pz/resolve-pullzone.ts create mode 100644 packages/cli/src/commands/pz/select.ts create mode 100644 packages/cli/src/commands/pz/show.ts diff --git a/packages/cli/src/commands/pz/clone.ts b/packages/cli/src/commands/pz/clone.ts new file mode 100644 index 0000000..664e48b --- /dev/null +++ b/packages/cli/src/commands/pz/clone.ts @@ -0,0 +1,131 @@ +import { createCoreClient } from "@bunny.net/openapi-client"; +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 { confirm, spinner } from "../../core/ui.ts"; +import { + PULL_ZONE_MANIFEST, + type PullZoneManifest, +} from "./constants.ts"; +import { resolvePullZoneId } from "./resolve-pullzone.ts"; + +interface EdgeRule { + Guid?: string; + ActionType: number; + ActionParameter1?: string; + ActionParameter2?: string; + Description?: string; + Enabled: boolean; + Triggers?: unknown[]; +} + +interface CloneArgs { + source?: string; + target?: string; +} + +export const pzCloneCommand = defineCommand({ + command: "clone ", + describe: "Clone a pull zone.", + examples: [ + ["$0 pz clone my-zone my-clone", "Clone by name"], + ["$0 pz clone 12345 my-clone", "Clone source by ID"], + ], + + builder: (yargs) => + yargs + .positional("source", { type: "string", describe: "Source pull zone name or ID" }) + .positional("target", { type: "string", describe: "New pull zone name" }), + + handler: async ({ source, target, profile, output, verbose, apiKey }) => { + if (!source || !target) { + throw new UserError("Source and target names are required."); + } + + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + const { id: sourceId } = await resolvePullZoneId(client, source); + + // Fetch full source zone + const fetchSpin = spinner("Fetching source pull zone..."); + fetchSpin.start(); + + const { data } = await client.GET("/pullzone/{id}", { + params: { path: { id: sourceId } }, + }); + + fetchSpin.stop(); + + const zone = data as Record | undefined; + if (!zone) { + throw new UserError(`Source pull zone ${sourceId} not found.`); + } + + // Clone: zero out identity fields, set new name + const cloneBody = { + ...zone, + Id: undefined, + Name: target, + EdgeScriptId: undefined, + MiddlewareScriptId: null, + Hostnames: [], + EdgeRules: undefined, + }; + + const createSpin = spinner("Creating clone..."); + createSpin.start(); + + const { data: newZoneData, error: createError } = await client.POST("/pullzone", { + body: cloneBody as any, + }); + + createSpin.stop(); + + if (createError) { + throw new UserError(`Failed to create clone: ${createError}`); + } + + const newZone = newZoneData as { Id?: number; Name?: string } | undefined; + const newId = newZone?.Id; + if (!newId) { + throw new UserError("Clone created but could not get the new zone ID."); + } + + // Copy edge rules + const sourceRules = (zone.EdgeRules ?? []) as EdgeRule[]; + if (sourceRules.length > 0) { + const ruleSpin = spinner(`Copying ${sourceRules.length} edge rules...`); + ruleSpin.start(); + + for (const rule of sourceRules) { + const { Guid: _, ...ruleBody } = rule; + await client.POST("/pullzone/{pullZoneId}/edgerules/addOrUpdate", { + params: { path: { pullZoneId: newId } }, + body: ruleBody as any, + }); + } + + ruleSpin.stop(); + } + + if (output === "json") { + logger.log(JSON.stringify({ id: newId, name: target, source_id: sourceId })); + return; + } + + logger.success(`Cloned "${source}" → "${target}" (ID: ${newId}).`); + + const shouldSelect = await confirm(`Set "${target}" as the active context?`); + if (shouldSelect) { + saveManifest(PULL_ZONE_MANIFEST, { + id: newId, + name: target, + }); + logger.success(`Selected ${target}.`); + } + }, +}); diff --git a/packages/cli/src/commands/pz/constants.ts b/packages/cli/src/commands/pz/constants.ts new file mode 100644 index 0000000..d60ccab --- /dev/null +++ b/packages/cli/src/commands/pz/constants.ts @@ -0,0 +1,8 @@ +export const ARG_PULL_ZONE_ID = "pull-zone-id"; + +export const PULL_ZONE_MANIFEST = "pullzone.json"; + +export interface PullZoneManifest { + id: number; + name?: string; +} diff --git a/packages/cli/src/commands/pz/create.ts b/packages/cli/src/commands/pz/create.ts new file mode 100644 index 0000000..f8fac21 --- /dev/null +++ b/packages/cli/src/commands/pz/create.ts @@ -0,0 +1,108 @@ +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 { saveManifest } from "../../core/manifest.ts"; +import { confirm, spinner } from "../../core/ui.ts"; +import { + PULL_ZONE_MANIFEST, + type PullZoneManifest, +} from "./constants.ts"; + +interface PullZone { + Id: number; + Name?: string | null; +} + +interface CreateArgs { + name?: string; + origin?: string; +} + +export const pzCreateCommand = defineCommand({ + command: "create ", + describe: "Create a new pull zone.", + examples: [ + ["$0 pz create my-zone https://origin.example.com", "Create a pull zone"], + ], + + builder: (yargs) => + yargs + .positional("name", { type: "string", describe: "Pull zone name" }) + .positional("origin", { + type: "string", + describe: "Origin URL (https:// is prepended if missing)", + }), + + handler: async ({ name, origin, profile, output, verbose, apiKey }) => { + if (!name || !origin) { + throw new UserError("Name and origin are required."); + } + + const url = origin.match(/^https?:\/\//) ? origin : `https://${origin}`; + + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + // Check if zone already exists + const spin = spinner("Checking for existing zone..."); + spin.start(); + + const { data } = await client.GET("/pullzone"); + const zones = (data ?? []) as PullZone[]; + + spin.stop(); + + const existing = zones.find( + (z) => z.Name?.toLowerCase() === name.toLowerCase(), + ); + if (existing) { + throw new UserError(`Pull zone "${name}" already exists (ID: ${existing.Id}).`); + } + + // Create + const createSpin = spinner("Creating pull zone..."); + createSpin.start(); + + await client.POST("/pullzone", { + body: { Name: name, OriginUrl: url } as any, + }); + + createSpin.stop(); + + // Find the new zone to get its ID + const findSpin = spinner("Fetching new zone..."); + findSpin.start(); + + const { data: updated } = await client.GET("/pullzone"); + const newZone = ((updated ?? []) as PullZone[]).find( + (z) => z.Name?.toLowerCase() === name.toLowerCase(), + ); + + findSpin.stop(); + + if (output === "json") { + logger.log(JSON.stringify({ name, origin: url, id: newZone?.Id ?? null })); + return; + } + + logger.success(`Pull zone "${name}" created.`); + + // Offer to select it + if (newZone) { + const shouldSelect = await confirm( + `Set "${name}" as the active context?`, + ); + if (shouldSelect) { + saveManifest(PULL_ZONE_MANIFEST, { + id: newZone.Id, + name: newZone.Name ?? undefined, + }); + logger.success(`Selected ${name}.`); + } + } + }, +}); diff --git a/packages/cli/src/commands/pz/delete.ts b/packages/cli/src/commands/pz/delete.ts new file mode 100644 index 0000000..36c2ec8 --- /dev/null +++ b/packages/cli/src/commands/pz/delete.ts @@ -0,0 +1,78 @@ +import { createCoreClient } from "@bunny.net/openapi-client"; +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 { removeManifest } from "../../core/manifest.ts"; +import { confirm, spinner } from "../../core/ui.ts"; +import { + PULL_ZONE_MANIFEST, +} from "./constants.ts"; +import { resolvePullZoneId } from "./resolve-pullzone.ts"; + +interface DeleteArgs { + "name-or-id"?: string; + force?: boolean; +} + +export const pzDeleteCommand = defineCommand({ + command: "delete [name-or-id]", + describe: "Delete a pull zone.", + examples: [ + ["$0 pz delete", "Delete selected pull zone"], + ["$0 pz delete my-zone", "Delete by name"], + ["$0 pz delete 12345", "Delete by ID"], + ["$0 pz delete --force", "Skip confirmation"], + ], + + builder: (yargs) => + yargs + .positional("name-or-id", { + type: "string", + describe: "Pull zone name or ID (uses selected one if omitted)", + }) + .option("force", { + alias: "f", + type: "boolean", + default: false, + describe: "Skip confirmation", + }), + + handler: async ({ "name-or-id": nameOrId, force, profile, output, verbose, apiKey }) => { + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + const { id: zoneId, name } = await resolvePullZoneId(client, nameOrId); + + const label = name ?? String(zoneId); + const ok = await confirm(`Delete pull zone "${label}"?`, { force }); + if (!ok) { + logger.log("Delete cancelled."); + return; + } + + const spin = spinner("Deleting pull zone..."); + spin.start(); + + const { error } = await client.DELETE("/pullzone/{id}", { + params: { path: { id: zoneId } }, + }); + + spin.stop(); + + if (error) { + throw new UserError(`Failed to delete pull zone: ${error}`); + } + + // Remove manifest if it pointed at the deleted zone + removeManifest(PULL_ZONE_MANIFEST); + + if (output === "json") { + logger.log(JSON.stringify({ id: zoneId, deleted: true })); + return; + } + + logger.success(`Pull zone "${label}" deleted.`); + }, +}); diff --git a/packages/cli/src/commands/pz/deselect.ts b/packages/cli/src/commands/pz/deselect.ts new file mode 100644 index 0000000..5c9d431 --- /dev/null +++ b/packages/cli/src/commands/pz/deselect.ts @@ -0,0 +1,23 @@ +import { defineCommand } from "../../core/define-command.ts"; +import { logger } from "../../core/logger.ts"; +import { removeManifest } from "../../core/manifest.ts"; +import { PULL_ZONE_MANIFEST } from "./constants.ts"; + +export const pzDeselectCommand = defineCommand({ + command: "deselect", + describe: "Clear the active pull zone context.", + examples: [ + ["$0 pz deselect", "Deselect the current pull zone"], + ], + + handler: async ({ output }) => { + removeManifest(PULL_ZONE_MANIFEST); + + if (output === "json") { + logger.log(JSON.stringify({ deselected: true })); + return; + } + + logger.success("Pull zone deselected."); + }, +}); diff --git a/packages/cli/src/commands/pz/index.ts b/packages/cli/src/commands/pz/index.ts new file mode 100644 index 0000000..7239708 --- /dev/null +++ b/packages/cli/src/commands/pz/index.ts @@ -0,0 +1,112 @@ +import type { CommandModule } from "yargs"; +import { defineNamespace } from "../../core/define-namespace.ts"; +import { pzCloneCommand } from "./clone.ts"; +import { pzCreateCommand } from "./create.ts"; +import { pzDeleteCommand } from "./delete.ts"; +import { pzDeselectCommand } from "./deselect.ts"; +import { pzListCommand } from "./list.ts"; +import { pzPurgeCommand } from "./purge.ts"; +import { pzSelectCommand } from "./select.ts"; +import { pzShowCommand } from "./show.ts"; + + + +const rulesList: CommandModule = { + command: "list ", + describe: "List edge rules for a pull zone.", + handler: () => {}, +}; + +const rulesAdd: CommandModule = { + command: "add ", + describe: "Add or update an edge rule from a JSON file.", + handler: () => {}, +}; + +const rulesExport: CommandModule = { + command: "export [file]", + describe: "Export an edge rule by name to JSON file or stdout.", + handler: () => {}, +}; + +const rulesCopy: CommandModule = { + command: "copy ", + describe: "Copy all edge rules from one pull zone to another.", + handler: () => {}, +}; + +const rulesDelete: CommandModule = { + command: "delete ", + describe: "Delete an edge rule by GUID.", + handler: () => {}, +}; + +const rulesToggle: CommandModule = { + command: "toggle ", + describe: "Enable or disable an edge rule.", + handler: () => {}, +}; + +const hostnamesList: CommandModule = { + command: "list ", + describe: "List hostnames for a pull zone.", + handler: () => {}, +}; + +const hostnamesAdd: CommandModule = { + command: "add ", + describe: "Add a hostname to a pull zone.", + handler: () => {}, +}; + +const hostnamesRemove: CommandModule = { + command: "remove ", + describe: "Remove a hostname from a pull zone.", + handler: () => {}, +}; + +const hostnamesCert: CommandModule = { + command: "cert ", + describe: "Provision a Let's Encrypt SSL certificate for a hostname.", + handler: () => {}, +}; + +const hostnamesForceSsl: CommandModule = { + command: "forcessl ", + describe: "Enable or disable Force SSL for a hostname.", + handler: () => {}, +}; + +const rulesNamespace = defineNamespace("rules", "Manage pull zone edge rules.", [ + rulesList, + rulesAdd, + rulesExport, + rulesCopy, + rulesDelete, + rulesToggle, +]); + +const hostnamesNamespace = defineNamespace("hostnames", "Manage pull zone hostnames.", [ + hostnamesList, + hostnamesAdd, + hostnamesRemove, + hostnamesCert, + hostnamesForceSsl, +]); + +export const pzNamespace = defineNamespace( + "pz", + "Manage pull zones.", + [ + pzListCommand, + pzCreateCommand, + pzCloneCommand, + pzDeleteCommand, + pzSelectCommand, + pzPurgeCommand, + pzShowCommand, + pzDeselectCommand, + rulesNamespace, + hostnamesNamespace, + ], +); diff --git a/packages/cli/src/commands/pz/list.ts b/packages/cli/src/commands/pz/list.ts new file mode 100644 index 0000000..454ef57 --- /dev/null +++ b/packages/cli/src/commands/pz/list.ts @@ -0,0 +1,64 @@ +import { createCoreClient } from "@bunny.net/openapi-client"; +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"; + +interface PullZone { + Id: number; + Name?: string | null; + OriginUrl?: string | null; + Enabled: boolean; + Suspended: boolean; +} + +export const pzListCommand = defineCommand({ + command: "list", + aliases: ["ls"] as const, + describe: "List all pull zones.", + examples: [ + ["$0 pz list", "List all pull zones"], + ["$0 pz list --output json", "JSON output"], + ], + + handler: async ({ profile, output, verbose, apiKey }) => { + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + const spin = spinner("Fetching pull zones..."); + spin.start(); + + const { data } = await client.GET("/pullzone"); + + spin.stop(); + + const zones = ((data ?? []) as PullZone[]).sort((a: PullZone, b: PullZone) => + (a.Name ?? "").localeCompare(b.Name ?? ""), + ); + + if (output === "json") { + logger.log(JSON.stringify(zones, null, 2)); + return; + } + + if (zones.length === 0) { + logger.info("No pull zones found."); + return; + } + + logger.log( + formatTable( + ["ID", "Name", "Origin", "Status"], + zones.map((zone: PullZone) => [ + String(zone.Id ?? ""), + zone.Name ?? "", + zone.OriginUrl ?? "", + zone.Suspended ? "Suspended" : zone.Enabled ? "Active" : "Disabled", + ]), + output, + ), + ); + }, +}); diff --git a/packages/cli/src/commands/pz/purge.ts b/packages/cli/src/commands/pz/purge.ts new file mode 100644 index 0000000..58e0a9e --- /dev/null +++ b/packages/cli/src/commands/pz/purge.ts @@ -0,0 +1,50 @@ +import { createCoreClient } from "@bunny.net/openapi-client"; +import { resolveConfig } from "../../config/index.ts"; +import { clientOptions } from "../../core/client-options.ts"; +import { defineCommand } from "../../core/define-command.ts"; +import { logger } from "../../core/logger.ts"; +import { spinner } from "../../core/ui.ts"; +import { resolvePullZoneId } from "./resolve-pullzone.ts"; + +interface PurgeArgs { + "name-or-id"?: string; +} + +export const pzPurgeCommand = defineCommand({ + command: "purge [name-or-id]", + describe: "Purge cached files for a pull zone.", + examples: [ + ["$0 pz purge", "Purge cache for selected pull zone"], + ["$0 pz purge my-zone", "Purge cache by name"], + ["$0 pz purge 12345", "Purge cache by ID"], + ], + + builder: (yargs) => + yargs.positional("name-or-id", { + type: "string", + describe: "Pull zone name or ID (uses selected one if omitted)", + }), + + handler: async ({ "name-or-id": nameOrId, profile, output, verbose, apiKey }) => { + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + const { id: zoneId } = await resolvePullZoneId(client, nameOrId); + + const spin = spinner("Purging cache..."); + spin.start(); + + await client.POST("/pullzone/{id}/purgeCache", { + params: { path: { id: zoneId } }, + }); + + spin.stop(); + + if (output === "json") { + logger.log(JSON.stringify({ id: zoneId, purged: true })); + return; + } + + logger.success(`Cache purged for pull zone ${zoneId}.`); + }, +}); diff --git a/packages/cli/src/commands/pz/resolve-pullzone.ts b/packages/cli/src/commands/pz/resolve-pullzone.ts new file mode 100644 index 0000000..499f727 --- /dev/null +++ b/packages/cli/src/commands/pz/resolve-pullzone.ts @@ -0,0 +1,76 @@ +import type { createCoreClient } from "@bunny.net/openapi-client"; +import { UserError } from "../../core/errors.ts"; +import { loadManifest } from "../../core/manifest.ts"; +import { spinner } from "../../core/ui.ts"; +import { + PULL_ZONE_MANIFEST, + type PullZoneManifest, +} from "./constants.ts"; + +interface PullZone { + Id: number; + Name?: string | null; +} + +export interface ResolvedPullZone { + id: number; + name?: string; + source: "argument" | "manifest"; +} + +/** + * Resolve a pull zone from a name-or-ID, or `.bunny/pullzone.json`. + * + * Tries name lookup first (case-insensitive). If no name matches and the + * value is numeric, falls back to treating it as an ID. No manifest + * fallback when a value is explicitly given. + */ +export async function resolvePullZoneId( + client: ReturnType, + idOrName: string | undefined, +): Promise { + if (idOrName) { + const isNumeric = /^\d+$/.test(idOrName); + + const spin = spinner( + isNumeric + ? `Fetching pull zone ${idOrName}...` + : `Looking up pull zone "${idOrName}"...`, + ); + spin.start(); + + const { data } = await client.GET("/pullzone"); + const zones = (data ?? []) as PullZone[]; + + spin.stop(); + + // Try name match first + const match = zones.find( + (z) => z.Name?.toLowerCase() === idOrName.toLowerCase(), + ); + + if (match) { + return { id: match.Id, name: match.Name ?? undefined, source: "argument" }; + } + + // Fall back to numeric ID + if (isNumeric) { + return { id: Number(idOrName), source: "argument" }; + } + + throw new UserError( + `Pull zone "${idOrName}" not found.`, + "Run `bunny pullzones list` to see available zones.", + ); + } + + const manifest = loadManifest(PULL_ZONE_MANIFEST); + if (manifest.id) { + return { id: manifest.id, name: manifest.name, source: "manifest" }; + } + + throw new UserError( + "No pull zone selected.", + 'Run "bunny pullzones select" or pass a zone name or ID.', + ); +} diff --git a/packages/cli/src/commands/pz/select.ts b/packages/cli/src/commands/pz/select.ts new file mode 100644 index 0000000..12bbd23 --- /dev/null +++ b/packages/cli/src/commands/pz/select.ts @@ -0,0 +1,108 @@ +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 { saveManifest } from "../../core/manifest.ts"; +import { spinner } from "../../core/ui.ts"; +import { + PULL_ZONE_MANIFEST, + type PullZoneManifest, +} from "./constants.ts"; +import { resolvePullZoneId } from "./resolve-pullzone.ts"; + +interface PullZone { + Id: number; + Name?: string | null; +} + +interface SelectArgs { + "name-or-id"?: string; +} + +export const pzSelectCommand = defineCommand({ + command: "select [name-or-id]", + describe: "Select a pull zone as the active context.", + examples: [ + ["$0 pz select", "Interactive selection"], + ["$0 pz select my-zone", "Select by name"], + ["$0 pz select 12345", "Select by ID"], + ], + + builder: (yargs) => + yargs.positional("name-or-id", { + type: "string", + describe: "Pull zone name or ID", + }), + + handler: async ({ "name-or-id": nameOrId, profile, output, verbose, apiKey }) => { + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + if (nameOrId) { + const { id, name } = await resolvePullZoneId(client, nameOrId); + + saveManifest(PULL_ZONE_MANIFEST, { + id, + name, + }); + + if (output === "json") { + logger.log(JSON.stringify({ id, name })); + return; + } + + logger.success(`Selected ${name ?? id}.`); + return; + } + + const spin = spinner("Fetching pull zones..."); + spin.start(); + + const { data } = await client.GET("/pullzone"); + + spin.stop(); + + const zones = (data ?? []) as PullZone[]; + + if (zones.length === 0) { + throw new UserError( + "No pull zones found.", + 'Run "bunny pz create" to create one.', + ); + } + + const sorted = zones.sort((a, b) => + (a.Name ?? "").localeCompare(b.Name ?? ""), + ); + + const { selected } = await prompts({ + type: "select", + name: "selected", + message: "Select a pull zone:", + choices: sorted.map((zone) => ({ + title: zone.Name ?? String(zone.Id), + value: zone, + })), + }); + + if (!selected) { + logger.log("Select cancelled."); + process.exit(1); + } + + saveManifest(PULL_ZONE_MANIFEST, { + id: selected.Id, + name: selected.Name ?? undefined, + }); + + if (output === "json") { + logger.log(JSON.stringify({ id: selected.Id, name: selected.Name })); + return; + } + + logger.success(`Selected ${selected.Name ?? selected.Id}.`); + }, +}); diff --git a/packages/cli/src/commands/pz/show.ts b/packages/cli/src/commands/pz/show.ts new file mode 100644 index 0000000..5c89c6d --- /dev/null +++ b/packages/cli/src/commands/pz/show.ts @@ -0,0 +1,89 @@ +import { createCoreClient } from "@bunny.net/openapi-client"; +import { resolveConfig } from "../../config/index.ts"; +import { clientOptions } from "../../core/client-options.ts"; +import { defineCommand } from "../../core/define-command.ts"; +import { formatKeyValue } from "../../core/format.ts"; +import { logger } from "../../core/logger.ts"; +import { resolvePullZoneId } from "./resolve-pullzone.ts"; + +interface PullZone { + Id: number; + Name?: string | null; + OriginUrl?: string | null; + Enabled: boolean; + Suspended: boolean; + Hostnames?: Array<{ Value?: string | null }> | null; + StorageZoneId?: number; + EdgeScriptId?: number; + ZoneSecurityEnabled?: boolean; + ZoneSecurityKey?: string | null; +} + +interface ShowArgs { + "name-or-id"?: string; +} + +export const pzShowCommand = defineCommand({ + command: "show [name-or-id]", + describe: "Show pull zone details.", + examples: [ + ["$0 pz show", "Show selected pull zone"], + ["$0 pz show my-zone", "Show by name"], + ["$0 pz show 12345", "Show by ID"], + ], + + builder: (yargs) => + yargs.positional("name-or-id", { + type: "string", + describe: "Pull zone name or ID (uses selected one if omitted)", + }), + + handler: async ({ "name-or-id": nameOrId, profile, output, verbose, apiKey }) => { + const config = resolveConfig(profile, apiKey, verbose); + const client = createCoreClient(clientOptions(config, verbose)); + + const { id: zoneId } = await resolvePullZoneId(client, nameOrId); + + const { data } = await client.GET("/pullzone/{id}", { + params: { path: { id: zoneId } }, + }); + + const zone = data as PullZone | undefined; + if (!zone) { + logger.error(`Pull zone ${zoneId} not found.`); + return; + } + + if (output === "json") { + logger.log(JSON.stringify(zone, null, 2)); + return; + } + + const hostnames = zone.Hostnames + ?.map((h) => h.Value) + .filter(Boolean) + .join(", ") ?? "none"; + + const status = zone.Suspended + ? "Suspended" + : zone.Enabled + ? "Active" + : "Disabled"; + + logger.log( + formatKeyValue( + [ + { key: "ID", value: String(zone.Id) }, + { key: "Name", value: zone.Name ?? "" }, + { key: "Origin", value: zone.OriginUrl ?? "" }, + { key: "Status", value: status }, + { key: "Hostnames", value: hostnames }, + { key: "Storage Zone ID", value: String(zone.StorageZoneId ?? "") }, + { key: "Edge Script ID", value: String(zone.EdgeScriptId ?? "") }, + { key: "Security", value: zone.ZoneSecurityEnabled ? "Enabled" : "Disabled" }, + ], + output, + ), + ); + }, +}); From b2c3762b8ad72e1d44ef8821ae72ee1c96eb2fac Mon Sep 17 00:00:00 2001 From: burstx86 Date: Wed, 3 Jun 2026 00:18:22 +0200 Subject: [PATCH 04/15] fix: reject numberic pull zone IDs not in account list --- packages/cli/src/commands/pz/resolve-pullzone.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/pz/resolve-pullzone.ts b/packages/cli/src/commands/pz/resolve-pullzone.ts index 499f727..54f07a0 100644 --- a/packages/cli/src/commands/pz/resolve-pullzone.ts +++ b/packages/cli/src/commands/pz/resolve-pullzone.ts @@ -53,9 +53,17 @@ export async function resolvePullZoneId( return { id: match.Id, name: match.Name ?? undefined, source: "argument" }; } - // Fall back to numeric ID + // Fall back to numeric ID — verify it exists in the list if (isNumeric) { - return { id: Number(idOrName), source: "argument" }; + const numericId = Number(idOrName); + const byId = zones.find((z) => z.Id === numericId); + if (byId) { + return { id: byId.Id, name: byId.Name ?? undefined, source: "argument" }; + } + throw new UserError( + `Pull zone with ID ${numericId} not found.`, + "Run `bunny pullzones list` to see available zones.", + ); } throw new UserError( From c9dc23d562f953dd3224ecf443938b29e2757f5e Mon Sep 17 00:00:00 2001 From: burstx86 Date: Wed, 3 Jun 2026 00:21:04 +0200 Subject: [PATCH 05/15] fix: changed pullzone to pz --- packages/cli/src/commands/pz/resolve-pullzone.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/pz/resolve-pullzone.ts b/packages/cli/src/commands/pz/resolve-pullzone.ts index 54f07a0..16e113f 100644 --- a/packages/cli/src/commands/pz/resolve-pullzone.ts +++ b/packages/cli/src/commands/pz/resolve-pullzone.ts @@ -62,13 +62,13 @@ export async function resolvePullZoneId( } throw new UserError( `Pull zone with ID ${numericId} not found.`, - "Run `bunny pullzones list` to see available zones.", + "Run `bunny pz list` to see available zones.", ); } throw new UserError( `Pull zone "${idOrName}" not found.`, - "Run `bunny pullzones list` to see available zones.", + "Run `bunny pz list` to see available zones.", ); } @@ -79,6 +79,6 @@ export async function resolvePullZoneId( throw new UserError( "No pull zone selected.", - 'Run "bunny pullzones select" or pass a zone name or ID.', + 'Run "bunny pz select" or pass a zone name or ID.', ); } From e020c265b53e0b79f587f21543a2cfd324721282 Mon Sep 17 00:00:00 2001 From: burstx86 Date: Wed, 3 Jun 2026 00:26:07 +0200 Subject: [PATCH 06/15] fix: removed unused subcommands --- packages/cli/src/commands/pz/index.ts | 90 +-------------------------- 1 file changed, 3 insertions(+), 87 deletions(-) diff --git a/packages/cli/src/commands/pz/index.ts b/packages/cli/src/commands/pz/index.ts index 7239708..9c4f11f 100644 --- a/packages/cli/src/commands/pz/index.ts +++ b/packages/cli/src/commands/pz/index.ts @@ -1,4 +1,3 @@ -import type { CommandModule } from "yargs"; import { defineNamespace } from "../../core/define-namespace.ts"; import { pzCloneCommand } from "./clone.ts"; import { pzCreateCommand } from "./create.ts"; @@ -9,90 +8,9 @@ import { pzPurgeCommand } from "./purge.ts"; import { pzSelectCommand } from "./select.ts"; import { pzShowCommand } from "./show.ts"; - - -const rulesList: CommandModule = { - command: "list ", - describe: "List edge rules for a pull zone.", - handler: () => {}, -}; - -const rulesAdd: CommandModule = { - command: "add ", - describe: "Add or update an edge rule from a JSON file.", - handler: () => {}, -}; - -const rulesExport: CommandModule = { - command: "export [file]", - describe: "Export an edge rule by name to JSON file or stdout.", - handler: () => {}, -}; - -const rulesCopy: CommandModule = { - command: "copy ", - describe: "Copy all edge rules from one pull zone to another.", - handler: () => {}, -}; - -const rulesDelete: CommandModule = { - command: "delete ", - describe: "Delete an edge rule by GUID.", - handler: () => {}, -}; - -const rulesToggle: CommandModule = { - command: "toggle ", - describe: "Enable or disable an edge rule.", - handler: () => {}, -}; - -const hostnamesList: CommandModule = { - command: "list ", - describe: "List hostnames for a pull zone.", - handler: () => {}, -}; - -const hostnamesAdd: CommandModule = { - command: "add ", - describe: "Add a hostname to a pull zone.", - handler: () => {}, -}; - -const hostnamesRemove: CommandModule = { - command: "remove ", - describe: "Remove a hostname from a pull zone.", - handler: () => {}, -}; - -const hostnamesCert: CommandModule = { - command: "cert ", - describe: "Provision a Let's Encrypt SSL certificate for a hostname.", - handler: () => {}, -}; - -const hostnamesForceSsl: CommandModule = { - command: "forcessl ", - describe: "Enable or disable Force SSL for a hostname.", - handler: () => {}, -}; - -const rulesNamespace = defineNamespace("rules", "Manage pull zone edge rules.", [ - rulesList, - rulesAdd, - rulesExport, - rulesCopy, - rulesDelete, - rulesToggle, -]); - -const hostnamesNamespace = defineNamespace("hostnames", "Manage pull zone hostnames.", [ - hostnamesList, - hostnamesAdd, - hostnamesRemove, - hostnamesCert, - hostnamesForceSsl, -]); +// TODO: implement rules and hostnames subcommands +// const rulesNamespace = defineNamespace("rules", "Manage pull zone edge rules.", [...]); +// const hostnamesNamespace = defineNamespace("hostnames", "Manage pull zone hostnames.", [...]); export const pzNamespace = defineNamespace( "pz", @@ -106,7 +24,5 @@ export const pzNamespace = defineNamespace( pzPurgeCommand, pzShowCommand, pzDeselectCommand, - rulesNamespace, - hostnamesNamespace, ], ); From 1be9bb4ca4f3415c0ad761eab3787da3a4777a59 Mon Sep 17 00:00:00 2001 From: burstx86 Date: Wed, 3 Jun 2026 00:30:20 +0200 Subject: [PATCH 07/15] fix: removeManifest only runs when manifests ID matches the deleted zone ID --- packages/cli/src/commands/pz/delete.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/pz/delete.ts b/packages/cli/src/commands/pz/delete.ts index 36c2ec8..0b82d77 100644 --- a/packages/cli/src/commands/pz/delete.ts +++ b/packages/cli/src/commands/pz/delete.ts @@ -4,10 +4,11 @@ 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 { removeManifest } from "../../core/manifest.ts"; +import { loadManifest, removeManifest } from "../../core/manifest.ts"; import { confirm, spinner } from "../../core/ui.ts"; import { PULL_ZONE_MANIFEST, + type PullZoneManifest, } from "./constants.ts"; import { resolvePullZoneId } from "./resolve-pullzone.ts"; @@ -65,8 +66,11 @@ export const pzDeleteCommand = defineCommand({ throw new UserError(`Failed to delete pull zone: ${error}`); } - // Remove manifest if it pointed at the deleted zone - removeManifest(PULL_ZONE_MANIFEST); + // Remove manifest only if it pointed at the deleted zone + const manifest = loadManifest(PULL_ZONE_MANIFEST); + if (manifest.id === zoneId) { + removeManifest(PULL_ZONE_MANIFEST); + } if (output === "json") { logger.log(JSON.stringify({ id: zoneId, deleted: true })); From 02e23abcd30c0f47cd18b365069ad603ffce3786 Mon Sep 17 00:00:00 2001 From: burstx86 Date: Wed, 3 Jun 2026 00:58:01 +0200 Subject: [PATCH 08/15] fix: define exact fields needed for cloning --- packages/cli/src/commands/pz/clone.ts | 77 ++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/commands/pz/clone.ts b/packages/cli/src/commands/pz/clone.ts index 664e48b..a1215f3 100644 --- a/packages/cli/src/commands/pz/clone.ts +++ b/packages/cli/src/commands/pz/clone.ts @@ -22,6 +22,73 @@ interface EdgeRule { Triggers?: unknown[]; } +/** Fields from the OpenAPI PullZoneAddModel schema — the only fields allowed in POST /pullzone. */ +const ADD_MODEL_FIELDS = [ + "OriginUrl", "AllowedReferrers", "BlockedReferrers", "BlockNoneReferrer", + "BlockedIps", "EnableGeoZoneUS", "EnableGeoZoneEU", "EnableGeoZoneASIA", + "EnableGeoZoneSA", "EnableGeoZoneAF", "BlockRootPathAccess", "BlockPostRequests", + "EnableQueryStringOrdering", "EnableWebPVary", "EnableAvifVary", + "EnableMobileVary", "EnableCountryCodeVary", "EnableCountryStateCodeVary", + "EnableHostnameVary", "EnableCacheSlice", "EnableWebPVary", + "ZoneSecurityEnabled", + "ZoneSecurityIncludeHashRemoteIP", "IgnoreQueryStrings", "MonthlyBandwidthLimit", + "AccessControlOriginHeaderExtensions", "EnableAccessControlOriginHeader", + "DisableCookies", "BudgetRedirectedCountries", "BlockedCountries", + "CacheControlMaxAgeOverride", "CacheControlPublicMaxAgeOverride", + "CacheControlBrowserMaxAgeOverride", "AddHostHeader", "AddCanonicalHeader", + "EnableLogging", "LoggingIPAnonymizationEnabled", "PermaCacheStorageZoneId", + "PermaCacheType", "AWSSigningEnabled", "AWSSigningKey", "AWSSigningRegionName", + "AWSSigningSecret", "EnableOriginShield", "OriginShieldZoneCode", + "EnableTLS1", "EnableTLS1_1", "CacheErrorResponses", "VerifyOriginSSL", + "LogForwardingEnabled", "LogForwardingHostname", "LogForwardingPort", + "LogForwardingToken", "LogForwardingProtocol", "LoggingSaveToStorage", + "LoggingStorageZoneId", "FollowRedirects", "ConnectionLimitPerIPCount", + "RequestLimit", "LimitRateAfter", "LimitRatePerSecond", "BurstSize", + "ErrorPageEnableCustomCode", "ErrorPageCustomCode", + "ErrorPageEnableStatuspageWidget", "ErrorPageStatuspageCode", + "ErrorPageWhitelabel", "OptimizerEnabled", "OptimizerTunnelEnabled", + "OptimizerDesktopMaxWidth", "OptimizerMobileMaxWidth", + "OptimizerImageQuality", "OptimizerMobileImageQuality", "OptimizerEnableWebP", + "OptimizerPrerenderHtml", "OptimizerEnableManipulationEngine", + "OptimizerMinifyCSS", "OptimizerMinifyJavaScript", + "OptimizerWatermarkEnabled", "OptimizerWatermarkUrl", + "OptimizerWatermarkPosition", "OptimizerWatermarkOffset", + "OptimizerWatermarkMinImageSize", "OptimizerAutomaticOptimizationEnabled", + "OptimizerClasses", "OptimizerForceClasses", "OptimizerStaticHtmlEnabled", + "OptimizerStaticHtmlWordPressPath", "OptimizerStaticHtmlWordPressBypassCookie", + "Type", "OriginRetries", "OriginConnectTimeout", "OriginResponseTimeout", + "UseStaleWhileUpdating", "UseStaleWhileOffline", "OriginRetry5XXResponses", + "OriginRetryConnectionTimeout", "OriginRetryResponseTimeout", + "OriginRetryDelay", "DnsOriginPort", "DnsOriginScheme", + "QueryStringVaryParameters", "OriginShieldEnableConcurrencyLimit", + "OriginShieldMaxConcurrentRequests", "EnableCookieVary", + "CookieVaryParameters", "EnableSafeHop", "OriginShieldQueueMaxWaitTime", + "OriginShieldMaxQueuedRequests", "UseBackgroundUpdate", "EnableAutoSSL", + "LogAnonymizationType", "StorageZoneId", "EdgeScriptId", "MiddlewareScriptId", + "EdgeScriptExecutionPhase", "OriginType", "MagicContainersAppId", + "MagicContainersEndpointId", "LogFormat", "LogForwardingFormat", + "ShieldDDosProtectionType", "ShieldDDosProtectionEnabled", + "OriginHostHeader", "EnableSmartCache", "EnableRequestCoalescing", + "RequestCoalescingTimeout", "DisableLetsEncrypt", "EnableBunnyImageAi", + "BunnyAiImageBlueprints", "PreloadingScreenEnabled", "PreloadingScreenCode", + "PreloadingScreenLogoUrl", "PreloadingScreenShowOnFirstVisit", + "PreloadingScreenTheme", "PreloadingScreenCodeEnabled", + "PreloadingScreenDelay", "RoutingFilters", "StickySessionType", + "StickySessionCookieName", "StickySessionClientHeaders", + "OptimizerEnableUpscaling", "EnableWebSockets", "MaxWebSocketConnections", + "Name", +] as const; + +function pick>(obj: T, keys: readonly string[]): Record { + const result: Record = {}; + for (const key of keys) { + if (key in obj) { + result[key] = obj[key]; + } + } + return result; +} + interface CloneArgs { source?: string; target?: string; @@ -50,7 +117,6 @@ export const pzCloneCommand = defineCommand({ const { id: sourceId } = await resolvePullZoneId(client, source); - // Fetch full source zone const fetchSpin = spinner("Fetching source pull zone..."); fetchSpin.start(); @@ -65,15 +131,14 @@ export const pzCloneCommand = defineCommand({ throw new UserError(`Source pull zone ${sourceId} not found.`); } - // Clone: zero out identity fields, set new name + // Clone: pick only PullZoneAddModel fields and set new name. + // Serves as a type-safe allow-list, excluding response-only fields + // (e.g. Id, Enabled, Suspended, UserId, MonthlyBandwidthUsed, etc.) const cloneBody = { - ...zone, - Id: undefined, + ...pick(zone, ADD_MODEL_FIELDS), Name: target, EdgeScriptId: undefined, MiddlewareScriptId: null, - Hostnames: [], - EdgeRules: undefined, }; const createSpin = spinner("Creating clone..."); From 65878d1b22184bbaa5b91ff530ba71eef5239e1d Mon Sep 17 00:00:00 2001 From: burstx86 Date: Wed, 3 Jun 2026 23:07:31 +0200 Subject: [PATCH 09/15] pullzone commands fixes --- packages/cli/src/commands/pz/create.ts | 46 ++++------------ packages/cli/src/commands/pz/delete.ts | 37 ++++++++----- packages/cli/src/commands/pz/index.ts | 10 ++-- .../src/commands/pz/{select.ts => link.ts} | 54 ++++++++----------- packages/cli/src/commands/pz/list.ts | 9 +--- packages/cli/src/commands/pz/purge.ts | 35 +++++++----- packages/cli/src/commands/pz/show.ts | 51 ++++++++---------- .../commands/pz/{deselect.ts => unlink.ts} | 12 ++--- 8 files changed, 113 insertions(+), 141 deletions(-) rename packages/cli/src/commands/pz/{select.ts => link.ts} (56%) rename packages/cli/src/commands/pz/{deselect.ts => unlink.ts} (56%) diff --git a/packages/cli/src/commands/pz/create.ts b/packages/cli/src/commands/pz/create.ts index f8fac21..bb77b86 100644 --- a/packages/cli/src/commands/pz/create.ts +++ b/packages/cli/src/commands/pz/create.ts @@ -1,5 +1,4 @@ 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"; @@ -12,11 +11,6 @@ import { type PullZoneManifest, } from "./constants.ts"; -interface PullZone { - Id: number; - Name?: string | null; -} - interface CreateArgs { name?: string; origin?: string; @@ -47,59 +41,39 @@ export const pzCreateCommand = defineCommand({ const config = resolveConfig(profile, apiKey, verbose); const client = createCoreClient(clientOptions(config, verbose)); - // Check if zone already exists - const spin = spinner("Checking for existing zone..."); - spin.start(); - - const { data } = await client.GET("/pullzone"); - const zones = (data ?? []) as PullZone[]; - - spin.stop(); - - const existing = zones.find( - (z) => z.Name?.toLowerCase() === name.toLowerCase(), - ); - if (existing) { - throw new UserError(`Pull zone "${name}" already exists (ID: ${existing.Id}).`); - } - // Create const createSpin = spinner("Creating pull zone..."); createSpin.start(); - await client.POST("/pullzone", { + const { data, error } = await client.POST("/pullzone", { body: { Name: name, OriginUrl: url } as any, }); createSpin.stop(); - // Find the new zone to get its ID - const findSpin = spinner("Fetching new zone..."); - findSpin.start(); - - const { data: updated } = await client.GET("/pullzone"); - const newZone = ((updated ?? []) as PullZone[]).find( - (z) => z.Name?.toLowerCase() === name.toLowerCase(), - ); + if (error) { + throw new UserError(`Failed to create pull zone: ${error}`); + } - findSpin.stop(); + const created = data as { Id?: number; Name?: string | null } | undefined; + const createdId = created?.Id; if (output === "json") { - logger.log(JSON.stringify({ name, origin: url, id: newZone?.Id ?? null })); + logger.log(JSON.stringify(created, null, 2)); return; } logger.success(`Pull zone "${name}" created.`); // Offer to select it - if (newZone) { + if (createdId) { const shouldSelect = await confirm( `Set "${name}" as the active context?`, ); if (shouldSelect) { saveManifest(PULL_ZONE_MANIFEST, { - id: newZone.Id, - name: newZone.Name ?? undefined, + id: createdId, + name: created?.Name ?? undefined, }); logger.success(`Selected ${name}.`); } diff --git a/packages/cli/src/commands/pz/delete.ts b/packages/cli/src/commands/pz/delete.ts index 0b82d77..b28acad 100644 --- a/packages/cli/src/commands/pz/delete.ts +++ b/packages/cli/src/commands/pz/delete.ts @@ -10,28 +10,26 @@ import { PULL_ZONE_MANIFEST, type PullZoneManifest, } from "./constants.ts"; -import { resolvePullZoneId } from "./resolve-pullzone.ts"; interface DeleteArgs { - "name-or-id"?: string; + id?: number; force?: boolean; } export const pzDeleteCommand = defineCommand({ - command: "delete [name-or-id]", + command: "delete [id]", describe: "Delete a pull zone.", examples: [ ["$0 pz delete", "Delete selected pull zone"], - ["$0 pz delete my-zone", "Delete by name"], - ["$0 pz delete 12345", "Delete by ID"], + ["$0 pz delete 12345", "Delete pull zone 12345"], ["$0 pz delete --force", "Skip confirmation"], ], builder: (yargs) => yargs - .positional("name-or-id", { - type: "string", - describe: "Pull zone name or ID (uses selected one if omitted)", + .positional("id", { + type: "number", + describe: "Pull zone ID (uses selected one if omitted)", }) .option("force", { alias: "f", @@ -40,14 +38,27 @@ export const pzDeleteCommand = defineCommand({ describe: "Skip confirmation", }), - handler: async ({ "name-or-id": nameOrId, force, profile, output, verbose, apiKey }) => { + handler: async ({ id, force, profile, output, verbose, apiKey }) => { + const zoneId = id ?? loadManifest(PULL_ZONE_MANIFEST).id; + if (!zoneId) { + throw new UserError( + "No pull zone specified.", + 'Pass a pull zone ID or run "bunny pz link" first.', + ); + } + const config = resolveConfig(profile, apiKey, verbose); const client = createCoreClient(clientOptions(config, verbose)); - const { id: zoneId, name } = await resolvePullZoneId(client, nameOrId); + const { data: zone } = await client.GET("/pullzone/{id}", { + params: { path: { id: zoneId } }, + }); + + const label = zone?.Name + ? `${zone.Name} (${zoneId})` + : String(zoneId); - const label = name ?? String(zoneId); - const ok = await confirm(`Delete pull zone "${label}"?`, { force }); + const ok = await confirm(`Delete pull zone ${label}?`, { force }); if (!ok) { logger.log("Delete cancelled."); return; @@ -77,6 +88,6 @@ export const pzDeleteCommand = defineCommand({ return; } - logger.success(`Pull zone "${label}" deleted.`); + logger.success(`Pull zone ${label} deleted.`); }, }); diff --git a/packages/cli/src/commands/pz/index.ts b/packages/cli/src/commands/pz/index.ts index 9c4f11f..07d6661 100644 --- a/packages/cli/src/commands/pz/index.ts +++ b/packages/cli/src/commands/pz/index.ts @@ -1,12 +1,11 @@ import { defineNamespace } from "../../core/define-namespace.ts"; -import { pzCloneCommand } from "./clone.ts"; import { pzCreateCommand } from "./create.ts"; import { pzDeleteCommand } from "./delete.ts"; -import { pzDeselectCommand } from "./deselect.ts"; +import { pzLinkCommand } from "./link.ts"; import { pzListCommand } from "./list.ts"; import { pzPurgeCommand } from "./purge.ts"; -import { pzSelectCommand } from "./select.ts"; import { pzShowCommand } from "./show.ts"; +import { pzUnlinkCommand } from "./unlink.ts"; // TODO: implement rules and hostnames subcommands // const rulesNamespace = defineNamespace("rules", "Manage pull zone edge rules.", [...]); @@ -18,11 +17,10 @@ export const pzNamespace = defineNamespace( [ pzListCommand, pzCreateCommand, - pzCloneCommand, pzDeleteCommand, - pzSelectCommand, + pzLinkCommand, pzPurgeCommand, pzShowCommand, - pzDeselectCommand, + pzUnlinkCommand, ], ); diff --git a/packages/cli/src/commands/pz/select.ts b/packages/cli/src/commands/pz/link.ts similarity index 56% rename from packages/cli/src/commands/pz/select.ts rename to packages/cli/src/commands/pz/link.ts index 12bbd23..66ff4fd 100644 --- a/packages/cli/src/commands/pz/select.ts +++ b/packages/cli/src/commands/pz/link.ts @@ -1,3 +1,4 @@ +import type { components } from "@bunny.net/openapi-client/generated/core.d.ts"; import { createCoreClient } from "@bunny.net/openapi-client"; import prompts from "prompts"; import { resolveConfig } from "../../config/index.ts"; @@ -11,50 +12,38 @@ import { PULL_ZONE_MANIFEST, type PullZoneManifest, } from "./constants.ts"; -import { resolvePullZoneId } from "./resolve-pullzone.ts"; -interface PullZone { - Id: number; - Name?: string | null; +interface LinkArgs { + id?: number; } -interface SelectArgs { - "name-or-id"?: string; -} - -export const pzSelectCommand = defineCommand({ - command: "select [name-or-id]", - describe: "Select a pull zone as the active context.", +export const pzLinkCommand = defineCommand({ + command: "link [id]", + describe: "Link the current directory to a pull zone.", examples: [ - ["$0 pz select", "Interactive selection"], - ["$0 pz select my-zone", "Select by name"], - ["$0 pz select 12345", "Select by ID"], + ["$0 pz link", "Interactive selection"], + ["$0 pz link 12345", "Link by ID"], ], builder: (yargs) => - yargs.positional("name-or-id", { - type: "string", - describe: "Pull zone name or ID", + yargs.positional("id", { + type: "number", + describe: "Pull zone ID", }), - handler: async ({ "name-or-id": nameOrId, profile, output, verbose, apiKey }) => { + handler: async ({ id, profile, output, verbose, apiKey }) => { const config = resolveConfig(profile, apiKey, verbose); const client = createCoreClient(clientOptions(config, verbose)); - if (nameOrId) { - const { id, name } = await resolvePullZoneId(client, nameOrId); - - saveManifest(PULL_ZONE_MANIFEST, { - id, - name, - }); + if (id) { + saveManifest(PULL_ZONE_MANIFEST, { id }); if (output === "json") { - logger.log(JSON.stringify({ id, name })); + logger.log(JSON.stringify({ id })); return; } - logger.success(`Selected ${name ?? id}.`); + logger.success(`Linked to pull zone ${id}.`); return; } @@ -65,7 +54,7 @@ export const pzSelectCommand = defineCommand({ spin.stop(); - const zones = (data ?? []) as PullZone[]; + const zones = (data ?? []) as components["schemas"]["PullZoneModel"][]; if (zones.length === 0) { throw new UserError( @@ -81,7 +70,7 @@ export const pzSelectCommand = defineCommand({ const { selected } = await prompts({ type: "select", name: "selected", - message: "Select a pull zone:", + message: "Link to a pull zone:", choices: sorted.map((zone) => ({ title: zone.Name ?? String(zone.Id), value: zone, @@ -89,20 +78,19 @@ export const pzSelectCommand = defineCommand({ }); if (!selected) { - logger.log("Select cancelled."); + logger.log("Link cancelled."); process.exit(1); } saveManifest(PULL_ZONE_MANIFEST, { id: selected.Id, - name: selected.Name ?? undefined, }); if (output === "json") { - logger.log(JSON.stringify({ id: selected.Id, name: selected.Name })); + logger.log(JSON.stringify({ id: selected.Id })); return; } - logger.success(`Selected ${selected.Name ?? selected.Id}.`); + logger.success(`Linked to ${selected.Name ?? selected.Id}.`); }, }); diff --git a/packages/cli/src/commands/pz/list.ts b/packages/cli/src/commands/pz/list.ts index 454ef57..912012b 100644 --- a/packages/cli/src/commands/pz/list.ts +++ b/packages/cli/src/commands/pz/list.ts @@ -1,3 +1,4 @@ +import type { components } from "@bunny.net/openapi-client/generated/core.d.ts"; import { createCoreClient } from "@bunny.net/openapi-client"; import { resolveConfig } from "../../config/index.ts"; import { clientOptions } from "../../core/client-options.ts"; @@ -6,13 +7,7 @@ import { formatTable } from "../../core/format.ts"; import { logger } from "../../core/logger.ts"; import { spinner } from "../../core/ui.ts"; -interface PullZone { - Id: number; - Name?: string | null; - OriginUrl?: string | null; - Enabled: boolean; - Suspended: boolean; -} +type PullZone = components["schemas"]["PullZoneModel"]; export const pzListCommand = defineCommand({ command: "list", diff --git a/packages/cli/src/commands/pz/purge.ts b/packages/cli/src/commands/pz/purge.ts index 58e0a9e..c5cc4cc 100644 --- a/packages/cli/src/commands/pz/purge.ts +++ b/packages/cli/src/commands/pz/purge.ts @@ -2,44 +2,55 @@ import { createCoreClient } from "@bunny.net/openapi-client"; 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 } from "../../core/manifest.ts"; import { spinner } from "../../core/ui.ts"; -import { resolvePullZoneId } from "./resolve-pullzone.ts"; +import { PULL_ZONE_MANIFEST, type PullZoneManifest } from "./constants.ts"; interface PurgeArgs { - "name-or-id"?: string; + id?: number; } export const pzPurgeCommand = defineCommand({ - command: "purge [name-or-id]", + command: "purge [id]", describe: "Purge cached files for a pull zone.", examples: [ ["$0 pz purge", "Purge cache for selected pull zone"], - ["$0 pz purge my-zone", "Purge cache by name"], - ["$0 pz purge 12345", "Purge cache by ID"], + ["$0 pz purge 12345", "Purge cache for pull zone 12345"], ], builder: (yargs) => - yargs.positional("name-or-id", { - type: "string", - describe: "Pull zone name or ID (uses selected one if omitted)", + yargs.positional("id", { + type: "number", + describe: "Pull zone ID (uses selected one if omitted)", }), - handler: async ({ "name-or-id": nameOrId, profile, output, verbose, apiKey }) => { + handler: async ({ id, profile, output, verbose, apiKey }) => { + const zoneId = id ?? loadManifest(PULL_ZONE_MANIFEST).id; + if (!zoneId) { + throw new UserError( + "No pull zone specified.", + 'Pass a pull zone ID or run "bunny pz link" first.', + ); + } + const config = resolveConfig(profile, apiKey, verbose); const client = createCoreClient(clientOptions(config, verbose)); - const { id: zoneId } = await resolvePullZoneId(client, nameOrId); - const spin = spinner("Purging cache..."); spin.start(); - await client.POST("/pullzone/{id}/purgeCache", { + const { error } = await client.POST("/pullzone/{id}/purgeCache", { params: { path: { id: zoneId } }, }); spin.stop(); + if (error) { + throw new UserError(`Failed to purge cache: ${error}`); + } + if (output === "json") { logger.log(JSON.stringify({ id: zoneId, purged: true })); return; diff --git a/packages/cli/src/commands/pz/show.ts b/packages/cli/src/commands/pz/show.ts index 5c89c6d..6988eb4 100644 --- a/packages/cli/src/commands/pz/show.ts +++ b/packages/cli/src/commands/pz/show.ts @@ -2,53 +2,46 @@ import { createCoreClient } from "@bunny.net/openapi-client"; 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 { resolvePullZoneId } from "./resolve-pullzone.ts"; - -interface PullZone { - Id: number; - Name?: string | null; - OriginUrl?: string | null; - Enabled: boolean; - Suspended: boolean; - Hostnames?: Array<{ Value?: string | null }> | null; - StorageZoneId?: number; - EdgeScriptId?: number; - ZoneSecurityEnabled?: boolean; - ZoneSecurityKey?: string | null; -} +import { loadManifest } from "../../core/manifest.ts"; +import { PULL_ZONE_MANIFEST, type PullZoneManifest } from "./constants.ts"; interface ShowArgs { - "name-or-id"?: string; + id?: number; } export const pzShowCommand = defineCommand({ - command: "show [name-or-id]", + command: "show [id]", describe: "Show pull zone details.", examples: [ ["$0 pz show", "Show selected pull zone"], - ["$0 pz show my-zone", "Show by name"], - ["$0 pz show 12345", "Show by ID"], + ["$0 pz show 12345", "Show pull zone 12345"], ], builder: (yargs) => - yargs.positional("name-or-id", { - type: "string", - describe: "Pull zone name or ID (uses selected one if omitted)", + yargs.positional("id", { + type: "number", + describe: "Pull zone ID (uses selected one if omitted)", }), - handler: async ({ "name-or-id": nameOrId, profile, output, verbose, apiKey }) => { + handler: async ({ id, profile, output, verbose, apiKey }) => { + const zoneId = id ?? loadManifest(PULL_ZONE_MANIFEST).id; + if (!zoneId) { + throw new UserError( + "No pull zone specified.", + 'Pass a pull zone ID or run "bunny pz link" first.', + ); + } + const config = resolveConfig(profile, apiKey, verbose); const client = createCoreClient(clientOptions(config, verbose)); - const { id: zoneId } = await resolvePullZoneId(client, nameOrId); - - const { data } = await client.GET("/pullzone/{id}", { + const { data: zone } = await client.GET("/pullzone/{id}", { params: { path: { id: zoneId } }, }); - const zone = data as PullZone | undefined; if (!zone) { logger.error(`Pull zone ${zoneId} not found.`); return; @@ -70,17 +63,19 @@ export const pzShowCommand = defineCommand({ ? "Active" : "Disabled"; + const security = zone.ZoneSecurityEnabled ? "Enabled" : "Disabled"; + logger.log( formatKeyValue( [ - { key: "ID", value: String(zone.Id) }, + { key: "ID", value: String(zone.Id ?? "") }, { key: "Name", value: zone.Name ?? "" }, { key: "Origin", value: zone.OriginUrl ?? "" }, { key: "Status", value: status }, { key: "Hostnames", value: hostnames }, { key: "Storage Zone ID", value: String(zone.StorageZoneId ?? "") }, { key: "Edge Script ID", value: String(zone.EdgeScriptId ?? "") }, - { key: "Security", value: zone.ZoneSecurityEnabled ? "Enabled" : "Disabled" }, + { key: "Security", value: security }, ], output, ), diff --git a/packages/cli/src/commands/pz/deselect.ts b/packages/cli/src/commands/pz/unlink.ts similarity index 56% rename from packages/cli/src/commands/pz/deselect.ts rename to packages/cli/src/commands/pz/unlink.ts index 5c9d431..e9a192a 100644 --- a/packages/cli/src/commands/pz/deselect.ts +++ b/packages/cli/src/commands/pz/unlink.ts @@ -3,21 +3,21 @@ import { logger } from "../../core/logger.ts"; import { removeManifest } from "../../core/manifest.ts"; import { PULL_ZONE_MANIFEST } from "./constants.ts"; -export const pzDeselectCommand = defineCommand({ - command: "deselect", - describe: "Clear the active pull zone context.", +export const pzUnlinkCommand = defineCommand({ + command: "unlink", + describe: "Unlink the current directory from its pull zone.", examples: [ - ["$0 pz deselect", "Deselect the current pull zone"], + ["$0 pz unlink", "Unlink the current pull zone"], ], handler: async ({ output }) => { removeManifest(PULL_ZONE_MANIFEST); if (output === "json") { - logger.log(JSON.stringify({ deselected: true })); + logger.log(JSON.stringify({ unlinked: true })); return; } - logger.success("Pull zone deselected."); + logger.success("Pull zone unlinked."); }, }); From 5f42010d0a6c48e3888d08ee1d94958d0d0ffe60 Mon Sep 17 00:00:00 2001 From: burstx86 Date: Thu, 4 Jun 2026 01:34:32 +0200 Subject: [PATCH 10/15] removed unused files --- packages/cli/src/commands/pz/clone.ts | 196 ------------------ .../cli/src/commands/pz/resolve-pullzone.ts | 84 -------- 2 files changed, 280 deletions(-) delete mode 100644 packages/cli/src/commands/pz/clone.ts delete mode 100644 packages/cli/src/commands/pz/resolve-pullzone.ts diff --git a/packages/cli/src/commands/pz/clone.ts b/packages/cli/src/commands/pz/clone.ts deleted file mode 100644 index a1215f3..0000000 --- a/packages/cli/src/commands/pz/clone.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { createCoreClient } from "@bunny.net/openapi-client"; -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 { confirm, spinner } from "../../core/ui.ts"; -import { - PULL_ZONE_MANIFEST, - type PullZoneManifest, -} from "./constants.ts"; -import { resolvePullZoneId } from "./resolve-pullzone.ts"; - -interface EdgeRule { - Guid?: string; - ActionType: number; - ActionParameter1?: string; - ActionParameter2?: string; - Description?: string; - Enabled: boolean; - Triggers?: unknown[]; -} - -/** Fields from the OpenAPI PullZoneAddModel schema — the only fields allowed in POST /pullzone. */ -const ADD_MODEL_FIELDS = [ - "OriginUrl", "AllowedReferrers", "BlockedReferrers", "BlockNoneReferrer", - "BlockedIps", "EnableGeoZoneUS", "EnableGeoZoneEU", "EnableGeoZoneASIA", - "EnableGeoZoneSA", "EnableGeoZoneAF", "BlockRootPathAccess", "BlockPostRequests", - "EnableQueryStringOrdering", "EnableWebPVary", "EnableAvifVary", - "EnableMobileVary", "EnableCountryCodeVary", "EnableCountryStateCodeVary", - "EnableHostnameVary", "EnableCacheSlice", "EnableWebPVary", - "ZoneSecurityEnabled", - "ZoneSecurityIncludeHashRemoteIP", "IgnoreQueryStrings", "MonthlyBandwidthLimit", - "AccessControlOriginHeaderExtensions", "EnableAccessControlOriginHeader", - "DisableCookies", "BudgetRedirectedCountries", "BlockedCountries", - "CacheControlMaxAgeOverride", "CacheControlPublicMaxAgeOverride", - "CacheControlBrowserMaxAgeOverride", "AddHostHeader", "AddCanonicalHeader", - "EnableLogging", "LoggingIPAnonymizationEnabled", "PermaCacheStorageZoneId", - "PermaCacheType", "AWSSigningEnabled", "AWSSigningKey", "AWSSigningRegionName", - "AWSSigningSecret", "EnableOriginShield", "OriginShieldZoneCode", - "EnableTLS1", "EnableTLS1_1", "CacheErrorResponses", "VerifyOriginSSL", - "LogForwardingEnabled", "LogForwardingHostname", "LogForwardingPort", - "LogForwardingToken", "LogForwardingProtocol", "LoggingSaveToStorage", - "LoggingStorageZoneId", "FollowRedirects", "ConnectionLimitPerIPCount", - "RequestLimit", "LimitRateAfter", "LimitRatePerSecond", "BurstSize", - "ErrorPageEnableCustomCode", "ErrorPageCustomCode", - "ErrorPageEnableStatuspageWidget", "ErrorPageStatuspageCode", - "ErrorPageWhitelabel", "OptimizerEnabled", "OptimizerTunnelEnabled", - "OptimizerDesktopMaxWidth", "OptimizerMobileMaxWidth", - "OptimizerImageQuality", "OptimizerMobileImageQuality", "OptimizerEnableWebP", - "OptimizerPrerenderHtml", "OptimizerEnableManipulationEngine", - "OptimizerMinifyCSS", "OptimizerMinifyJavaScript", - "OptimizerWatermarkEnabled", "OptimizerWatermarkUrl", - "OptimizerWatermarkPosition", "OptimizerWatermarkOffset", - "OptimizerWatermarkMinImageSize", "OptimizerAutomaticOptimizationEnabled", - "OptimizerClasses", "OptimizerForceClasses", "OptimizerStaticHtmlEnabled", - "OptimizerStaticHtmlWordPressPath", "OptimizerStaticHtmlWordPressBypassCookie", - "Type", "OriginRetries", "OriginConnectTimeout", "OriginResponseTimeout", - "UseStaleWhileUpdating", "UseStaleWhileOffline", "OriginRetry5XXResponses", - "OriginRetryConnectionTimeout", "OriginRetryResponseTimeout", - "OriginRetryDelay", "DnsOriginPort", "DnsOriginScheme", - "QueryStringVaryParameters", "OriginShieldEnableConcurrencyLimit", - "OriginShieldMaxConcurrentRequests", "EnableCookieVary", - "CookieVaryParameters", "EnableSafeHop", "OriginShieldQueueMaxWaitTime", - "OriginShieldMaxQueuedRequests", "UseBackgroundUpdate", "EnableAutoSSL", - "LogAnonymizationType", "StorageZoneId", "EdgeScriptId", "MiddlewareScriptId", - "EdgeScriptExecutionPhase", "OriginType", "MagicContainersAppId", - "MagicContainersEndpointId", "LogFormat", "LogForwardingFormat", - "ShieldDDosProtectionType", "ShieldDDosProtectionEnabled", - "OriginHostHeader", "EnableSmartCache", "EnableRequestCoalescing", - "RequestCoalescingTimeout", "DisableLetsEncrypt", "EnableBunnyImageAi", - "BunnyAiImageBlueprints", "PreloadingScreenEnabled", "PreloadingScreenCode", - "PreloadingScreenLogoUrl", "PreloadingScreenShowOnFirstVisit", - "PreloadingScreenTheme", "PreloadingScreenCodeEnabled", - "PreloadingScreenDelay", "RoutingFilters", "StickySessionType", - "StickySessionCookieName", "StickySessionClientHeaders", - "OptimizerEnableUpscaling", "EnableWebSockets", "MaxWebSocketConnections", - "Name", -] as const; - -function pick>(obj: T, keys: readonly string[]): Record { - const result: Record = {}; - for (const key of keys) { - if (key in obj) { - result[key] = obj[key]; - } - } - return result; -} - -interface CloneArgs { - source?: string; - target?: string; -} - -export const pzCloneCommand = defineCommand({ - command: "clone ", - describe: "Clone a pull zone.", - examples: [ - ["$0 pz clone my-zone my-clone", "Clone by name"], - ["$0 pz clone 12345 my-clone", "Clone source by ID"], - ], - - builder: (yargs) => - yargs - .positional("source", { type: "string", describe: "Source pull zone name or ID" }) - .positional("target", { type: "string", describe: "New pull zone name" }), - - handler: async ({ source, target, profile, output, verbose, apiKey }) => { - if (!source || !target) { - throw new UserError("Source and target names are required."); - } - - const config = resolveConfig(profile, apiKey, verbose); - const client = createCoreClient(clientOptions(config, verbose)); - - const { id: sourceId } = await resolvePullZoneId(client, source); - - const fetchSpin = spinner("Fetching source pull zone..."); - fetchSpin.start(); - - const { data } = await client.GET("/pullzone/{id}", { - params: { path: { id: sourceId } }, - }); - - fetchSpin.stop(); - - const zone = data as Record | undefined; - if (!zone) { - throw new UserError(`Source pull zone ${sourceId} not found.`); - } - - // Clone: pick only PullZoneAddModel fields and set new name. - // Serves as a type-safe allow-list, excluding response-only fields - // (e.g. Id, Enabled, Suspended, UserId, MonthlyBandwidthUsed, etc.) - const cloneBody = { - ...pick(zone, ADD_MODEL_FIELDS), - Name: target, - EdgeScriptId: undefined, - MiddlewareScriptId: null, - }; - - const createSpin = spinner("Creating clone..."); - createSpin.start(); - - const { data: newZoneData, error: createError } = await client.POST("/pullzone", { - body: cloneBody as any, - }); - - createSpin.stop(); - - if (createError) { - throw new UserError(`Failed to create clone: ${createError}`); - } - - const newZone = newZoneData as { Id?: number; Name?: string } | undefined; - const newId = newZone?.Id; - if (!newId) { - throw new UserError("Clone created but could not get the new zone ID."); - } - - // Copy edge rules - const sourceRules = (zone.EdgeRules ?? []) as EdgeRule[]; - if (sourceRules.length > 0) { - const ruleSpin = spinner(`Copying ${sourceRules.length} edge rules...`); - ruleSpin.start(); - - for (const rule of sourceRules) { - const { Guid: _, ...ruleBody } = rule; - await client.POST("/pullzone/{pullZoneId}/edgerules/addOrUpdate", { - params: { path: { pullZoneId: newId } }, - body: ruleBody as any, - }); - } - - ruleSpin.stop(); - } - - if (output === "json") { - logger.log(JSON.stringify({ id: newId, name: target, source_id: sourceId })); - return; - } - - logger.success(`Cloned "${source}" → "${target}" (ID: ${newId}).`); - - const shouldSelect = await confirm(`Set "${target}" as the active context?`); - if (shouldSelect) { - saveManifest(PULL_ZONE_MANIFEST, { - id: newId, - name: target, - }); - logger.success(`Selected ${target}.`); - } - }, -}); diff --git a/packages/cli/src/commands/pz/resolve-pullzone.ts b/packages/cli/src/commands/pz/resolve-pullzone.ts deleted file mode 100644 index 16e113f..0000000 --- a/packages/cli/src/commands/pz/resolve-pullzone.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { createCoreClient } from "@bunny.net/openapi-client"; -import { UserError } from "../../core/errors.ts"; -import { loadManifest } from "../../core/manifest.ts"; -import { spinner } from "../../core/ui.ts"; -import { - PULL_ZONE_MANIFEST, - type PullZoneManifest, -} from "./constants.ts"; - -interface PullZone { - Id: number; - Name?: string | null; -} - -export interface ResolvedPullZone { - id: number; - name?: string; - source: "argument" | "manifest"; -} - -/** - * Resolve a pull zone from a name-or-ID, or `.bunny/pullzone.json`. - * - * Tries name lookup first (case-insensitive). If no name matches and the - * value is numeric, falls back to treating it as an ID. No manifest - * fallback when a value is explicitly given. - */ -export async function resolvePullZoneId( - client: ReturnType, - idOrName: string | undefined, -): Promise { - if (idOrName) { - const isNumeric = /^\d+$/.test(idOrName); - - const spin = spinner( - isNumeric - ? `Fetching pull zone ${idOrName}...` - : `Looking up pull zone "${idOrName}"...`, - ); - spin.start(); - - const { data } = await client.GET("/pullzone"); - const zones = (data ?? []) as PullZone[]; - - spin.stop(); - - // Try name match first - const match = zones.find( - (z) => z.Name?.toLowerCase() === idOrName.toLowerCase(), - ); - - if (match) { - return { id: match.Id, name: match.Name ?? undefined, source: "argument" }; - } - - // Fall back to numeric ID — verify it exists in the list - if (isNumeric) { - const numericId = Number(idOrName); - const byId = zones.find((z) => z.Id === numericId); - if (byId) { - return { id: byId.Id, name: byId.Name ?? undefined, source: "argument" }; - } - throw new UserError( - `Pull zone with ID ${numericId} not found.`, - "Run `bunny pz list` to see available zones.", - ); - } - - throw new UserError( - `Pull zone "${idOrName}" not found.`, - "Run `bunny pz list` to see available zones.", - ); - } - - const manifest = loadManifest(PULL_ZONE_MANIFEST); - if (manifest.id) { - return { id: manifest.id, name: manifest.name, source: "manifest" }; - } - - throw new UserError( - "No pull zone selected.", - 'Run "bunny pz select" or pass a zone name or ID.', - ); -} From cd96e9fe6989d634c7c3b3efbd7eb8a91387fc55 Mon Sep 17 00:00:00 2001 From: burstx86 Date: Fri, 5 Jun 2026 02:37:18 +0200 Subject: [PATCH 11/15] Error check --- packages/cli/src/commands/pz/link.ts | 30 +++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/pz/link.ts b/packages/cli/src/commands/pz/link.ts index 66ff4fd..00f70af 100644 --- a/packages/cli/src/commands/pz/link.ts +++ b/packages/cli/src/commands/pz/link.ts @@ -36,14 +36,38 @@ export const pzLinkCommand = defineCommand({ const client = createCoreClient(clientOptions(config, verbose)); if (id) { - saveManifest(PULL_ZONE_MANIFEST, { id }); + const spin = spinner("Fetching pull zone..."); + spin.start(); + + let zone: components["schemas"]["PullZoneModel"] | undefined; + + try { + const { data } = await client.GET("/pullzone/{id}", { + params: { path: { id } }, + }); + zone = data as components["schemas"]["PullZoneModel"] | undefined; + } catch (err: unknown) { + spin.stop(); + const msg = err instanceof Error ? err.message : String(err); + throw new UserError(`Fetching failed: ${msg}`); + } + + spin.stop(); + + if (!zone) { + throw new UserError(`Pull zone ${id} not found.`); + } + + saveManifest(PULL_ZONE_MANIFEST, { + id: zone.Id ?? id, + }); if (output === "json") { - logger.log(JSON.stringify({ id })); + logger.log(JSON.stringify({ id: zone.Id ?? id })); return; } - logger.success(`Linked to pull zone ${id}.`); + logger.success(`Linked to ${zone.Name ?? zone.Id ?? id}.`); return; } From 74a48a4f72bdecb9d3fe1408484a16b615fcbc3f Mon Sep 17 00:00:00 2001 From: burstx86 Date: Fri, 5 Jun 2026 02:52:49 +0200 Subject: [PATCH 12/15] Removed unused ARG_PULL_ZONE_ID --- packages/cli/src/commands/pz/constants.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/cli/src/commands/pz/constants.ts b/packages/cli/src/commands/pz/constants.ts index d60ccab..30d3aca 100644 --- a/packages/cli/src/commands/pz/constants.ts +++ b/packages/cli/src/commands/pz/constants.ts @@ -1,5 +1,3 @@ -export const ARG_PULL_ZONE_ID = "pull-zone-id"; - export const PULL_ZONE_MANIFEST = "pullzone.json"; export interface PullZoneManifest { From 674528e9c8460e45cc48871266fee58b9e79c3df Mon Sep 17 00:00:00 2001 From: burstx86 Date: Fri, 5 Jun 2026 02:56:48 +0200 Subject: [PATCH 13/15] Added spinner to show.ts --- packages/cli/src/commands/pz/show.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/commands/pz/show.ts b/packages/cli/src/commands/pz/show.ts index 6988eb4..82c4e3c 100644 --- a/packages/cli/src/commands/pz/show.ts +++ b/packages/cli/src/commands/pz/show.ts @@ -1,3 +1,4 @@ +import type { components } from "@bunny.net/openapi-client/generated/core.d.ts"; import { createCoreClient } from "@bunny.net/openapi-client"; import { resolveConfig } from "../../config/index.ts"; import { clientOptions } from "../../core/client-options.ts"; @@ -6,6 +7,7 @@ import { UserError } from "../../core/errors.ts"; import { formatKeyValue } from "../../core/format.ts"; import { logger } from "../../core/logger.ts"; import { loadManifest } from "../../core/manifest.ts"; +import { spinner } from "../../core/ui.ts"; import { PULL_ZONE_MANIFEST, type PullZoneManifest } from "./constants.ts"; interface ShowArgs { @@ -38,13 +40,26 @@ export const pzShowCommand = defineCommand({ const config = resolveConfig(profile, apiKey, verbose); const client = createCoreClient(clientOptions(config, verbose)); - const { data: zone } = await client.GET("/pullzone/{id}", { - params: { path: { id: zoneId } }, - }); + const spin = spinner("Fetching pull zone..."); + spin.start(); + + let zone: components["schemas"]["PullZoneModel"] | undefined; + + try { + const { data } = await client.GET("/pullzone/{id}", { + params: { path: { id: zoneId } }, + }); + zone = data as components["schemas"]["PullZoneModel"] | undefined; + } catch (err: unknown) { + spin.stop(); + const msg = err instanceof Error ? err.message : String(err); + throw new UserError(`Fetching failed: ${msg}`); + } + + spin.stop(); if (!zone) { - logger.error(`Pull zone ${zoneId} not found.`); - return; + throw new UserError(`Pull zone ${zoneId} not found.`); } if (output === "json") { From b9a80f4e6c89b1538d9ec22b1c8c8de7bc364a1a Mon Sep 17 00:00:00 2001 From: burstx86 Date: Fri, 5 Jun 2026 03:03:26 +0200 Subject: [PATCH 14/15] Switched exit for return --- packages/cli/src/commands/pz/link.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/pz/link.ts b/packages/cli/src/commands/pz/link.ts index 00f70af..49ad57b 100644 --- a/packages/cli/src/commands/pz/link.ts +++ b/packages/cli/src/commands/pz/link.ts @@ -103,7 +103,7 @@ export const pzLinkCommand = defineCommand({ if (!selected) { logger.log("Link cancelled."); - process.exit(1); + return; } saveManifest(PULL_ZONE_MANIFEST, { From f69b0f59e6920fcecf205d4d2434f2b79b63ea62 Mon Sep 17 00:00:00 2001 From: burstx86 Date: Fri, 5 Jun 2026 21:21:36 +0200 Subject: [PATCH 15/15] Added error catch --- packages/cli/src/commands/pz/delete.ts | 8 +++++++- packages/cli/src/commands/pz/show.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/pz/delete.ts b/packages/cli/src/commands/pz/delete.ts index b28acad..d86b76b 100644 --- a/packages/cli/src/commands/pz/delete.ts +++ b/packages/cli/src/commands/pz/delete.ts @@ -50,10 +50,16 @@ export const pzDeleteCommand = defineCommand({ const config = resolveConfig(profile, apiKey, verbose); const client = createCoreClient(clientOptions(config, verbose)); - const { data: zone } = await client.GET("/pullzone/{id}", { + const { data: zone, error: getError } = await client.GET("/pullzone/{id}", { params: { path: { id: zoneId } }, }); + if (getError) { + throw new UserError( + `Failed to fetch pull zone ${zoneId}: ${getError.message ?? getError}`, + ); + } + const label = zone?.Name ? `${zone.Name} (${zoneId})` : String(zoneId); diff --git a/packages/cli/src/commands/pz/show.ts b/packages/cli/src/commands/pz/show.ts index 82c4e3c..3be1d4b 100644 --- a/packages/cli/src/commands/pz/show.ts +++ b/packages/cli/src/commands/pz/show.ts @@ -53,7 +53,7 @@ export const pzShowCommand = defineCommand({ } catch (err: unknown) { spin.stop(); const msg = err instanceof Error ? err.message : String(err); - throw new UserError(`Fetching failed: ${msg}`); + throw new UserError(`Failed to fetch pull zone ${zoneId}: ${msg}`); } spin.stop();