1import { ApiClient, commitTargetFor } from "../lib/api.ts";2import { buildBundle, formatBytes } from "../lib/bundle.ts";3import type { Config } from "../lib/config.ts";4import { lookupBySource, recordPush } from "../lib/paths.ts";5import { amber, bold, cyan, dim, emit, fail, green, parseTtl, ttlLeft } from "../lib/fmt.ts";6import { publishBundle } from "../lib/publish.ts";78export function formatPushPolicy(9 expiresAt: number | null,10 protectedSite: boolean,11 feedback = false,12): string {13 const expiry = expiresAt === null ? "forever" : `expires in ${ttlLeft(expiresAt)}`;14 const passwordStatus = protectedSite ? "password protected" : "no password";15 return `${expiry}, ${passwordStatus}${feedback ? ", feedback on" : ""}`;16}1718export async function cmdPush(19 config: Config,20 path: string | undefined,21 targetRaw: string | undefined,22 ttlRaw: string | undefined,23 password: string | undefined,24 noPassword: boolean,25 newSite: boolean,26 app?: { contentSecurityPolicy?: string },27 feedback?: boolean,28): Promise<void> {29 if (!path) {30 fail(31 "usage: nzip site push <dir|file> [target] [--new] [--ttl 14d|forever] [--password PW | --no-password] [--feedback | --no-feedback]",32 );33 }34 if (password !== undefined && noPassword) {35 fail("choose either --password or --no-password, not both");36 }37 if (newSite && targetRaw !== undefined) {38 fail("--new cannot be combined with an explicit target");39 }40 const api = new ApiClient(config);4142 let effectiveTargetRaw = targetRaw;43 if (effectiveTargetRaw === undefined && !newSite) {44 const matches = await lookupBySource(path);45 if (matches.length === 1) {46 effectiveTargetRaw = matches[0].address;47 } else if (matches.length > 1) {48 fail(49 `multiple existing sites were pushed from this source: ${50 matches.map((entry) => entry.address).join(", ")51 }`,52 "pass the intended target explicitly, or pass --new to create a separate site",53 );54 }55 }5657 // Resolve (and vault-guard) the target up front — before any bundling or58 // upload — so a disallowed vault is refused without touching the network.59 const rawAddress = effectiveTargetRaw !== undefined &&60 /^[0-9a-f]{4}$/.test(effectiveTargetRaw);61 const needsStatus = effectiveTargetRaw === undefined ||62 (effectiveTargetRaw !== undefined && !rawAddress && !effectiveTargetRaw.includes(":")) ||63 (rawAddress && config.allowVaults !== undefined);64 const status = needsStatus ? await api.status() : undefined;65 const targetConfig = (() => {66 if (67 effectiveTargetRaw !== undefined &&68 (/^[0-9a-f]{4}$/.test(effectiveTargetRaw) || effectiveTargetRaw.includes(":"))69 ) {70 return config;71 }72 const defaultVault = status?.defaultVaults?.temporary ?? config.defaultVault;73 if (!defaultVault) {74 fail("server has no temporary default vault — specify vault:alias or configure one");75 }76 return { ...config, defaultVault };77 })();78 const target = (() => {79 try {80 return commitTargetFor(effectiveTargetRaw, targetConfig, status?.vaults);81 } catch (e) {82 return fail((e as Error).message);83 }84 })();8586 const bundle = await buildBundle(path).catch((e) => fail((e as Error).message));87 const fileCount = Object.keys(bundle.manifest.files).length;88 emit(() => {89 console.log(90 dim(91 ` bundling ${path} … ${fileCount} files, ${formatBytes(bundle.totalBytes)}`,92 ),93 );94 for (const w of bundle.warnings) console.log(` ${amber("!")} ${w}`);95 }, undefined);9697 const ttl = ttlRaw === undefined ? undefined : parseTtl(ttlRaw);98 const passwordPolicy = noPassword ? null : password;99 const res = await publishBundle(api, bundle, {100 target,101 ttl,102 password: passwordPolicy,103 ...(feedback === undefined ? {} : { feedback }),104 ...(app === undefined ? {} : { app }),105 }, {106 onProgress: ({ message }) => emit(() => console.log(dim(` ${message}`))),107 });108109 // Breadcrumb: remember which directory this machine pushed from so110 // `nzip site where` can find it later. Best-effort — never fail a push over it.111 await recordPush({112 address: res.address,113 vault: "vault" in target ? target.vault : undefined,114 alias: res.alias,115 path,116 url: res.url,117 expiresAt: res.expiresAt,118 rebindSource: targetRaw !== undefined,119 }).catch(() => {});120121 const label = res.alias ? `${"vault" in target ? target.vault : "?"}:${res.alias}` : res.address;122 const policy = formatPushPolicy(res.expiresAt, res.protected, res.feedback === true);123 emit(124 () =>125 console.log(126 `${green("✓")} pushed ${bold(label)} → ${cyan(res.url)} ${127 dim(`(${policy}, push #${res.seq})`)128 }`,129 ),130 {131 ok: true,132 ...res,133 },134 );135}