clone

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

codex/agent-interface
nzip / cli / commands / push.ts
push.ts 3.3K
1import { ApiClient, commitTargetFor } from "../lib/api.ts";2import { buildBundle, formatBytes } from "../lib/bundle.ts";3import type { Config } from "../lib/config.ts";4import { recordPush } from "../lib/paths.ts";5import { amber, bold, cyan, dim, emit, fail, green, parseTtl, ttlLeft } from "../lib/fmt.ts";67const UPLOAD_CONCURRENCY = 6;89export async function cmdPush(10 config: Config,11 path: string | undefined,12 targetRaw: string | undefined,13 ttlRaw: string | undefined,14): Promise<void> {15 if (!path) fail("usage: nzip push <dir|file> [target] [--ttl 14d|forever]");16 const api = new ApiClient(config);1718 // Resolve (and vault-guard) the target up front — before any bundling or19 // upload — so a disallowed vault is refused without touching the network.20 const target = (() => {21 try {22 return commitTargetFor(targetRaw, config);23 } catch (e) {24 return fail((e as Error).message);25 }26 })();2728 const bundle = await buildBundle(path).catch((e) => fail((e as Error).message));29 const fileCount = Object.keys(bundle.manifest.files).length;30 emit(() => {31 console.log(dim(` bundling ${path} … ${fileCount} files, ${formatBytes(bundle.totalBytes)}`));32 for (const w of bundle.warnings) console.log(` ${amber("!")} ${w}`);33 }, undefined);3435 const prep = await api.prepare(bundle.manifest);36 const dedupCount = bundle.blobs.size - prep.missing.length;37 const missingBytes = prep.missing.reduce((n, h) => n + (bundle.blobs.get(h)?.length ?? 0), 0);38 emit(() =>39 console.log(40 dim(41 ` manifest ${prep.manifestHash.slice(0, 8)} — ${prep.missing.length} new blobs (${42 formatBytes(missingBytes)43 }), ${dedupCount} deduped`,44 ),45 ), undefined);4647 // Upload missing blobs with bounded concurrency.48 let done = 0;49 const queue = [...prep.missing];50 const workers = Array.from(51 { length: Math.min(UPLOAD_CONCURRENCY, queue.length) },52 async () => {53 for (let h = queue.shift(); h !== undefined; h = queue.shift()) {54 const bytes = bundle.blobs.get(h);55 if (!bytes) fail(`internal: missing blob bytes for ${h}`);56 await api.uploadBlob(h, bytes);57 done++;58 }59 },60 );61 await Promise.all(workers);62 if (prep.missing.length > 0) {63 emit(() => console.log(dim(` uploaded ${done}/${prep.missing.length}`)));64 }6566 const ttl = ttlRaw === undefined ? undefined : parseTtl(ttlRaw);67 const res = await api.commit({ manifest: bundle.manifest, target, ttl });6869 // Breadcrumb: remember which directory this machine pushed from so70 // `nzip where` can find it later. Best-effort — never fail a push over it.71 await recordPush({72 address: res.address,73 vault: "vault" in target ? target.vault : undefined,74 alias: res.alias,75 path,76 url: res.url,77 expiresAt: res.expiresAt,78 }).catch(() => {});7980 const label = res.alias ? `${"vault" in target ? target.vault : "?"}:${res.alias}` : res.address;81 const expiry = res.expiresAt === null ? "forever" : `expires in ${ttlLeft(res.expiresAt)}`;82 emit(83 () =>84 console.log(85 `${green("✓")} pushed ${bold(label)} → ${cyan(res.url)} ${86 dim(`(${expiry}, push #${res.seq})`)87 }`,88 ),89 {90 ok: true,91 ...res,92 files: fileCount,93 newBlobs: prep.missing.length,94 dedupedBlobs: dedupCount,95 warnings: bundle.warnings,96 },97 );98}