clone

push a directory of html from the terminal and get a four-character url back. a deno cli on cloudflare

feat/source-stable-push
nzip / cli / commands / sites.ts
sites.ts 6.4K
1// Site lifecycle operations plus the global deployment status command.23import type { SiteDetail } from "@nzip/shared";4import { ApiClient, resolveCliTargetWithStatus } from "../lib/api.ts";5import type { Config } from "../lib/config.ts";6import { forget, reconcile } from "../lib/paths.ts";7import {8 ago,9 amber,10 bold,11 cyan,12 dim,13 emit,14 fail,15 green,16 parseTtl,17 table,18 ttlLeft,19} from "../lib/fmt.ts";2021async function targetOrFail(22 raw: string | undefined,23 config: Config,24 api: ApiClient,25 usage: string,26): Promise<string> {27 if (!raw) fail(usage);28 try {29 return await resolveCliTargetWithStatus(raw, config, api);30 } catch (e) {31 return fail((e as Error).message);32 }33}3435function emitSiteDetail(site: SiteDetail): void {36 const name = site.alias ? `${site.vault}:${site.alias}` : site.address;37 emit(38 () =>39 console.log(40 `${bold(name)} ${cyan(site.url)} ${site.manifestHash.slice(0, 8)} ${41 site.expiresAt === null ? green("forever") : `expires in ${ttlLeft(site.expiresAt)}`42 }${site.protected ? ` ${amber("🔒")}` : ""}`,43 ),44 { ok: true, ...site },45 );46}4748export async function cmdSiteShow(49 config: Config,50 raw: string | undefined,51): Promise<void> {52 const api = new ApiClient(config);53 const target = await targetOrFail(raw, config, api, "usage: nzip site show <target>");54 emitSiteDetail(await api.siteDetail(target));55}5657export async function cmdSitePolicy(58 config: Config,59 raw: string | undefined,60 ttlRaw: string | undefined,61 password: string | undefined,62 noPassword: boolean,63): Promise<void> {64 const api = new ApiClient(config);65 const target = await targetOrFail(66 raw,67 config,68 api,69 "usage: nzip site policy <target> [--ttl 14d|forever] [--password PW | --no-password]",70 );71 if (password !== undefined && noPassword) {72 fail("choose either --password or --no-password, not both");73 }74 const patch: { ttl?: number | "forever"; password?: string | null } = {};75 if (ttlRaw !== undefined) patch.ttl = parseTtl(ttlRaw);76 if (noPassword) patch.password = null;77 else if (password !== undefined) patch.password = password;78 if (Object.keys(patch).length === 0) {79 fail(80 "site policy requires --ttl, --password, or --no-password",81 "to publish revised content, run: nzip site push <dir|file> <target>",82 );83 }84 emitSiteDetail(await api.patchSite(target, patch));85}8687export async function cmdLs(88 config: Config,89 vault: string | undefined,90): Promise<void> {91 const api = new ApiClient(config);92 const sites = await api.listSites(vault);93 // The server is authoritative for what still exists — drop breadcrumbs for94 // sites it no longer lists (expired, deleted here or on another machine).95 await reconcile(new Set(sites.map((s) => s.address)), vault).catch(() => {});96 emit(() => {97 if (sites.length === 0) {98 console.log(dim(vault ? `no sites in ${vault}` : "no sites"));99 return;100 }101 console.log(table(102 ["ADDR", "VAULT", "ALIAS", "PUSHED", "EXPIRES", "MANIFEST"],103 sites.map((s) => [104 cyan(s.address),105 s.vault,106 (s.alias ?? dim("—")) + (s.protected ? " 🔒" : ""),107 ago(s.updatedAt),108 s.expiresAt === null ? green("forever") : ttlLeft(s.expiresAt),109 dim(s.manifestHash.slice(0, 8)),110 ]),111 ));112 }, { ok: true, sites });113}114115export async function cmdRm(116 config: Config,117 raw: string | undefined,118 yes: boolean,119): Promise<void> {120 const api = new ApiClient(config);121 const target = await targetOrFail(raw, config, api, "usage: nzip site rm <target> [--yes]");122 const site = await api.siteDetail(target);123 const name = site.alias ? `${site.vault}:${site.alias}` : site.address;124 if (!yes) {125 const answer = prompt(`delete ${name} (${site.url})? [y/N]`);126 if (answer?.toLowerCase() !== "y") {127 emit(() => console.log(dim("aborted")), {128 ok: false,129 error: "aborted (pass --yes to skip the prompt)",130 });131 return;132 }133 }134 await api.deleteSite(target);135 await forget({136 address: site.address,137 vault: site.vault,138 alias: site.alias ?? undefined,139 }).catch(140 () => {},141 );142 emit(143 () =>144 console.log(145 `${green("✓")} removed ${bold(name)} — content becomes eligible for GC`,146 ),147 { ok: true, removed: site.address, alias: site.alias, vault: site.vault },148 );149}150151export async function cmdStatus(config: Config): Promise<void> {152 const api = new ApiClient(config);153 const status = await api.status();154 emit(() => {155 console.log(`${green("●")} ${config.server} ${dim(`v${status.version}`)}`);156 console.log(157 ` sites: ${status.siteCount}${158 status.expiringSoon > 0 ? ` ${amber(`(${status.expiringSoon} expiring <48h)`)}` : ""159 }`,160 );161 console.log(162 ` defaults: temporary=${status.defaultVaults.temporary ?? "unset"} permanent=${163 status.defaultVaults.permanent ?? "unset"164 } global ttl=${status.globalDefaultTtl}d`,165 );166 if (status.vaults.length > 0) {167 console.log(table(168 ["SLOT", "VAULT", "SITES", "DEFAULT TTL", "DEFAULT FOR", "DESCRIPTION"],169 status.vaults.map((170 v,171 ) => [172 `0x${v.slot.toString(16)}`,173 v.name,174 String(v.siteCount),175 String(v.effectiveDefaultTtl),176 v.defaultFor.join(","),177 v.description ?? "",178 ]),179 ));180 }181 }, { ...status, server: config.server });182}183184export async function cmdRevert(185 config: Config,186 raw: string | undefined,187 toSeq: number | undefined,188 list: boolean,189): Promise<void> {190 const api = new ApiClient(config);191 const target = await targetOrFail(192 raw,193 config,194 api,195 "usage: nzip site revert <target> [--to N] [--list]",196 );197198 if (list) {199 const site = await api.siteDetail(target);200 emit(() =>201 console.log(table(202 ["PUSH", "MANIFEST", "WHEN", "NOTE"],203 site.history.map((h) => [204 h.manifestHash === site.manifestHash ? bold(`#${h.seq} ←`) : `#${h.seq}`,205 dim(h.manifestHash.slice(0, 8)),206 ago(h.pushedAt),207 h.note ?? "",208 ]),209 )), { ok: true, current: site.manifestHash, history: site.history });210 return;211 }212213 const res = await api.revert(target, toSeq);214 emit(215 () =>216 console.log(217 `${green("✓")} ${bold(raw!)} reverted to push #${res.revertedTo} (${218 dim(res.manifestHash.slice(0, 8))219 }) — ${cyan(res.url)}`,220 ),221 { ok: true, ...res },222 );223}