1import { gzipSync } from "node:zlib";23export type TarFile = { path: string; bytes: Uint8Array; executable: boolean };45678const STRIPPED = new Set([9 "koh.build",10 "kepr.build",11 "Cargo.lock",12 "package-lock.json",13 "yarn.lock",14 "pnpm-lock.yaml",15 "bun.lockb",16 "go.sum",17 "LOG.md",18 "CONTINUITY.md",19 "AGENTS.md",20 ".kohignore",21 ".gitignore",22]);2324export function isStripped(path: string): boolean {25 return STRIPPED.has(path) || path === ".agent" || path.startsWith(".agent/");26}2728const encoder = new TextEncoder();29const BLOCK = 512;30313233343536export function buildTarball(root: string, files: TarFile[]): Uint8Array<ArrayBuffer> {37 const entries = files38 .filter((file) => file.path !== "" && !isStripped(file.path))39 .map((file) => ({ ...file, name: encoder.encode(`${root}/${file.path}`) }))40 .sort((a, b) => Buffer.compare(a.name, b.name));41 const blocks: Uint8Array[] = [header(encoder.encode(root), 0, "dir")];42 for (const entry of entries) {43 blocks.push(header(entry.name, entry.bytes.length, entry.executable ? "exec" : "file"));44 blocks.push(entry.bytes, new Uint8Array((BLOCK - (entry.bytes.length % BLOCK)) % BLOCK));45 }46 blocks.push(new Uint8Array(2 * BLOCK));47 const gzip = gzipSync(Buffer.concat(blocks), { level: 6 });48 49 gzip[9] = 3;50 return gzip;51}52535455function header(name: Uint8Array, size: number, kind: "dir" | "file" | "exec"): Uint8Array {56 const h = new Uint8Array(BLOCK);57 const isDir = kind === "dir";58 const maxName = isDir ? 98 : 99;59 let base = name;60 if (name.length > maxName) {61 const split = splitAt(name, maxName);62 if (split === null)63 throw new Error(`path too long for a tarball: ${new TextDecoder().decode(name)}`);64 h.set(name.subarray(0, split), 345);65 base = name.subarray(split + 1);66 }67 h.set(base);68 if (isDir) h[base.length] = 0x2f;69 const ascii = (text: string, at: number) => h.set(encoder.encode(text), at);70 ascii(kind === "file" ? "0000644\0" : "0000755\0", 100);71 ascii("0000000\0", 108);72 ascii("0000000\0", 116);73 if (!isDir) ascii(`${size.toString(8).padStart(11, "0")}\0`, 124);74 ascii("00000000000\0", 136);75 ascii(" ", 148);76 ascii(isDir ? "5" : "0", 156);77 ascii("ustar\0", 257);78 ascii("00", 263);79 const sum = h.reduce((total, byte) => total + byte, 0);80 ascii(`${sum.toString(8).padStart(6, "0")}\0 `, 148);81 return h;82}838485function splitAt(path: Uint8Array, maxName: number): number | null {86 const min = path.length - maxName - 1;87 for (let i = Math.min(path.length - 1, 154); i >= Math.max(min, 0); i--) {88 if (path[i] === 0x2f) return i;89 }90 return null;91}