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 / lib / paths.ts
paths.ts 4.4K
1// Local breadcrumb registry: which directory on *this* machine produced each2// pushed site. The server never learns the source path, so we keep a small3// map here so `nzip where <target>` can answer "where does that live on disk".4//5// Kept clean by three forces: expired entries are dropped on every write,6// `nzip rm` deletes its entry, and `nzip ls` reconciles against the live set.78import { join, resolve } from "@std/path";9import { configDir } from "./config.ts";1011export interface PathEntry {12 address: string; // 4-hex, canonical identity of the site13 vault?: string;14 alias?: string;15 path: string; // absolute source dir/file this machine pushed from16 url: string;17 expiresAt: number | null; // unix seconds, null = permanent18 pushedAt: number; // unix seconds19}2021/** address → entry */22type Registry = Record<string, PathEntry>;2324function pathsFile(): string {25 return join(configDir(), "paths.json");26}2728function nowSeconds(): number {29 return Math.floor(Date.now() / 1000);30}3132async function load(): Promise<Registry> {33 try {34 const parsed = JSON.parse(await Deno.readTextFile(pathsFile())) as Registry;35 return parsed && typeof parsed === "object" ? parsed : {};36 } catch {37 return {};38 }39}4041async function save(reg: Registry): Promise<void> {42 await Deno.mkdir(configDir(), { recursive: true });43 const file = pathsFile();44 await Deno.writeTextFile(file, JSON.stringify(reg, null, 2) + "\n");45 await Deno.chmod(file, 0o600);46}4748/** Drop entries whose TTL has already elapsed — the server has GC'd them. */49function pruneExpired(reg: Registry, now = nowSeconds()): Registry {50 for (const [addr, e] of Object.entries(reg)) {51 if (e.expiresAt !== null && e.expiresAt <= now) delete reg[addr];52 }53 return reg;54}5556/** Record (or refresh) the source path for a site just pushed from `path`. */57export async function recordPush(entry: {58 address: string;59 vault?: string;60 alias?: string | null;61 path: string;62 url: string;63 expiresAt: number | null;64}): Promise<void> {65 const reg = pruneExpired(await load());66 // An alias points at exactly one address at a time; clear any stale67 // entry that still claims this vault:alias under a different address.68 if (entry.vault && entry.alias) {69 for (const [addr, e] of Object.entries(reg)) {70 if (addr !== entry.address && e.vault === entry.vault && e.alias === entry.alias) {71 delete reg[addr];72 }73 }74 }75 let abs: string;76 try {77 abs = Deno.realPathSync(entry.path); // canonical absolute path; file exists now78 } catch {79 abs = resolve(entry.path);80 }81 reg[entry.address] = {82 address: entry.address,83 vault: entry.vault,84 alias: entry.alias ?? undefined,85 path: abs,86 url: entry.url,87 expiresAt: entry.expiresAt,88 pushedAt: nowSeconds(),89 };90 await save(reg);91}9293/** Look up a tracked source path by address or by vault:alias. */94export async function lookup(95 q: { address?: string; vault?: string; alias?: string },96): Promise<PathEntry | null> {97 const reg = pruneExpired(await load());98 if (q.address && reg[q.address]) return reg[q.address];99 if (q.vault && q.alias) {100 for (const e of Object.values(reg)) {101 if (e.vault === q.vault && e.alias === q.alias) return e;102 }103 }104 return null;105}106107/** Forget a single site (called from `nzip rm`). */108export async function forget(109 q: { address?: string; vault?: string; alias?: string },110): Promise<void> {111 const reg = await load();112 let changed = false;113 for (const [addr, e] of Object.entries(reg)) {114 const hit = (q.address && addr === q.address) ||115 (q.vault && q.alias && e.vault === q.vault && e.alias === q.alias);116 if (hit) {117 delete reg[addr];118 changed = true;119 }120 }121 if (changed) await save(reg);122}123124/**125 * Drop registry entries the server no longer lists. `live` is the set of126 * addresses returned by the API; when `scopeVault` is set only entries in127 * that vault are eligible for removal (the listing was vault-scoped).128 */129export async function reconcile(live: Set<string>, scopeVault?: string): Promise<void> {130 const reg = pruneExpired(await load());131 let changed = false;132 for (const [addr, e] of Object.entries(reg)) {133 if (scopeVault && e.vault !== scopeVault) continue;134 if (!live.has(addr)) {135 delete reg[addr];136 changed = true;137 }138 }139 // pruneExpired above may also have removed entries — persist either way.140 await save(reg);141 void changed;142}