1// share, ls, rm, status, revert — the site-management commands.23import { ApiClient, resolveCliTarget } from "../lib/api.ts";4import type { Config } from "../lib/config.ts";5import { forget, reconcile } from "../lib/paths.ts";6import {7 ago,8 amber,9 bold,10 cyan,11 dim,12 emit,13 fail,14 green,15 parseTtl,16 table,17 ttlLeft,18} from "../lib/fmt.ts";1920function targetOrFail(raw: string | undefined, config: Config, usage: string): string {21 if (!raw) fail(usage);22 try {23 return resolveCliTarget(raw, config);24 } catch (e) {25 return fail((e as Error).message);26 }27}2829export async function cmdShare(30 config: Config,31 raw: string | undefined,32 ttlRaw: string | undefined,33 password: string | undefined,34 noPassword: boolean,35): Promise<void> {36 const target = targetOrFail(37 raw,38 config,39 "usage: nzip share <target> [--ttl 14d|forever] [--password PW | --no-password]",40 );41 const api = new ApiClient(config);42 const patch: { ttl?: number | "forever"; password?: string | null } = {};43 if (ttlRaw !== undefined) patch.ttl = parseTtl(ttlRaw);44 if (noPassword) patch.password = null;45 else if (password !== undefined) patch.password = password;46 const site = Object.keys(patch).length === 047 ? await api.siteDetail(target)48 : await api.patchSite(target, patch);49 const name = site.alias ? `${site.vault}:${site.alias}` : site.address;50 emit(51 () =>52 console.log(53 `${bold(name)} ${cyan(site.url)} ${site.manifestHash.slice(0, 8)} ${54 site.expiresAt === null ? green("forever") : `expires in ${ttlLeft(site.expiresAt)}`55 }${site.protected ? ` ${amber("🔒")}` : ""}`,56 ),57 { ok: true, ...site },58 );59}6061export async function cmdLs(config: Config, vault: string | undefined): Promise<void> {62 const api = new ApiClient(config);63 const sites = await api.listSites(vault);64 // The server is authoritative for what still exists — drop breadcrumbs for65 // sites it no longer lists (expired, deleted here or on another machine).66 await reconcile(new Set(sites.map((s) => s.address)), vault).catch(() => {});67 emit(() => {68 if (sites.length === 0) {69 console.log(dim(vault ? `no sites in ${vault}` : "no sites"));70 return;71 }72 console.log(table(73 ["ADDR", "VAULT", "ALIAS", "PUSHED", "EXPIRES", "MANIFEST"],74 sites.map((s) => [75 cyan(s.address),76 s.vault,77 (s.alias ?? dim("—")) + (s.protected ? " 🔒" : ""),78 ago(s.updatedAt),79 s.expiresAt === null ? green("forever") : ttlLeft(s.expiresAt),80 dim(s.manifestHash.slice(0, 8)),81 ]),82 ));83 }, { ok: true, sites });84}8586export async function cmdRm(config: Config, raw: string | undefined, yes: boolean): Promise<void> {87 const target = targetOrFail(raw, config, "usage: nzip rm <target> [--yes]");88 const api = new ApiClient(config);89 const site = await api.siteDetail(target);90 const name = site.alias ? `${site.vault}:${site.alias}` : site.address;91 if (!yes) {92 const answer = prompt(`delete ${name} (${site.url})? [y/N]`);93 if (answer?.toLowerCase() !== "y") {94 emit(() => console.log(dim("aborted")), {95 ok: false,96 error: "aborted (pass --yes to skip the prompt)",97 });98 return;99 }100 }101 await api.deleteSite(target);102 await forget({ address: site.address, vault: site.vault, alias: site.alias ?? undefined }).catch(103 () => {},104 );105 emit(106 () => console.log(`${green("✓")} removed ${bold(name)} — content becomes eligible for GC`),107 { ok: true, removed: site.address, alias: site.alias, vault: site.vault },108 );109}110111export async function cmdStatus(config: Config): Promise<void> {112 const api = new ApiClient(config);113 const status = await api.status();114 emit(() => {115 console.log(`${green("●")} ${config.server} ${dim(`v${status.version}`)}`);116 console.log(117 ` sites: ${status.siteCount}${118 status.expiringSoon > 0 ? ` ${amber(`(${status.expiringSoon} expiring <48h)`)}` : ""119 }`,120 );121 if (status.vaults.length > 0) {122 console.log(table(123 ["SLOT", "VAULT", "SITES"],124 status.vaults.map((v) => [`0x${v.slot.toString(16)}`, v.name, String(v.siteCount)]),125 ));126 }127 }, { ...status, server: config.server });128}129130export async function cmdRevert(131 config: Config,132 raw: string | undefined,133 toSeq: number | undefined,134 list: boolean,135): Promise<void> {136 const target = targetOrFail(raw, config, "usage: nzip revert <target> [--to N] [--list]");137 const api = new ApiClient(config);138139 if (list) {140 const site = await api.siteDetail(target);141 emit(() =>142 console.log(table(143 ["PUSH", "MANIFEST", "WHEN", "NOTE"],144 site.history.map((h) => [145 h.manifestHash === site.manifestHash ? bold(`#${h.seq} ←`) : `#${h.seq}`,146 dim(h.manifestHash.slice(0, 8)),147 ago(h.pushedAt),148 h.note ?? "",149 ]),150 )), { ok: true, current: site.manifestHash, history: site.history });151 return;152 }153154 const res = await api.revert(target, toSeq);155 emit(156 () =>157 console.log(158 `${green("✓")} ${bold(raw!)} reverted to push #${res.revertedTo} (${159 dim(res.manifestHash.slice(0, 8))160 }) — ${cyan(res.url)}`,161 ),162 { ok: true, ...res },163 );164}