"""Bundle Blurt's source into a portable zip — e.g. to move it to another machine — leaving out the 200 MB+ .venv, caches, logs, and API key files. Cross-platform and dependency-free (stdlib only, no shell, no git), so it runs the same way on Linux and Windows: uv run export.py [output.zip] # default: /blurt-export.zip It only reads, never modifying this directory, so it is safe to run while Blurt is recording. On the other machine: unzip, then `uv sync` to rebuild .venv for that OS. """ import os import sys import tempfile import zipfile from pathlib import Path PROJECT_DIR = Path(__file__).resolve().parent ARCHIVE_ROOT = "blurt" # top-level folder inside the zip # Never worth copying. API key files are excluded by name so secrets are never # swept in, even though they sit right next to the source. EXCLUDE_DIRS = { ".git", ".venv", "__pycache__", ".claude", "dist", "build", ".ruff_cache", ".pytest_cache", } EXCLUDE_NAMES = {"api_key.txt", "openrouter_key.txt"} EXCLUDE_SUFFIXES = {".pyc", ".pyo"} def _included_files(): for root, dirs, files in os.walk(PROJECT_DIR): dirs[:] = [ d for d in dirs if d not in EXCLUDE_DIRS and not d.endswith(".egg-info") ] for name in files: if name in EXCLUDE_NAMES or Path(name).suffix in EXCLUDE_SUFFIXES: continue yield Path(root) / name def main(): default_out = Path(tempfile.gettempdir()) / "blurt-export.zip" out = (Path(sys.argv[1]) if len(sys.argv) > 1 else default_out).resolve() out.parent.mkdir(parents=True, exist_ok=True) count = 0 with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zf: for path in _included_files(): if path.resolve() == out: # don't zip the output into itself continue arcname = Path(ARCHIVE_ROOT) / path.relative_to(PROJECT_DIR) zf.write(path, arcname) count += 1 print(f"Wrote {out} ({count} files, {out.stat().st_size // 1024} KB)") if __name__ == "__main__": main()