Metadata-Version: 2.4
Name: utfbundle
Version: 0.1.1
Summary: Pack a directory of text files into one self-extracting Python script, and unpack it back.
Project-URL: Homepage, https://github.com/griffijf/utfbundle
Project-URL: Issues, https://github.com/griffijf/utfbundle/issues
Author-email: Jeff Griffin <pypi@brainslugsolutions.com>
License-Expression: MIT
License-File: LICENSE
Keywords: archive,bundle,chatgpt-upload,claude-projects,cli,directory,directory-to-file,flat-file-upload,github-gist-upload,llm-upload,reversible-archive,self-extracting,single-file,text
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: System :: Archiving
Classifier: Topic :: Utilities
Requires-Python: >=3.8
Requires-Dist: pathspec
Description-Content-Type: text/markdown

# utfbundle

Packs a directory of text files into a single Python script, and unpacks
it back into an identical directory tree.

Useful for uploading a folder to GitHub Gist, ChatGPT, Claude Projects, 
or any other tool that only accepts flat files, single files, or text 
blocks. These interfaces typically flatten directory structure on upload, 
so anything nested under subdirectories loses its path once it's in context.

Reaching for an archiver _might_ be an option in some of these cases,
unless text files inside the archive need to be read by a bot or AI 
integration.

`utfbundle` writes a directory's contents into one `.py` file, with
each original file's path and content stored inside it as plain text
where possible, or base64 for anything that isn't valid UTF-8. Running
that file writes every file back out, unchanged, to a directory of your
choosing. Packing and unpacking are inverses.  Files come back out
byte-for-byte identical, whichever form they were stored in.

Not intended to be a general-purpose archiver. No compression, no 
encryption. `tar` or `zip` already cover those use cases. This is 
specifically for the case where the constraint is "one plain text file,
exactly reversible" including directories that have a few binary files 
mixed in with the text.

## Install

```bash
pip install utfbundle
```

This installs the `utfbundle` command (and pulls in `pathspec` as a
dependency automatically).

## Requirements

- Python 3.8+
- [`pathspec`](https://pypi.org/project/pathspec/) (installed automatically with the package above; `pip install pathspec` if running from source)

## Quick start

Pack the current directory:

```bash
[python3] utfbundle
```

This writes `<dirname>_bundle.py` in the current directory.

Reconstruct the tree later by running the bundle:

```bash
python3 <dirname>_bundle.py
```

With no arguments, it writes back into `.`. Packing `.` and running the
resulting bundle from the same location are complements. Pass a path to
extract elsewhere instead:

```bash
python3 <dirname>_bundle.py /path/to/restore/into
```

You can also pack a specific directory rather than the current one:

```bash
[python3] utfbundle path/to/project -o project_bundle.py
```

Here, since a named directory was packed rather than `.`, the bundle's
default destination is a subdirectory named after it.  Running
`project_bundle.py` with no arguments elsewhere recreates
`path/to/project/` as `project/`.

## Choosing what's included: `.bundlespec`

By default, packing includes everything except common junk (`.git/`,
`node_modules/`, `target/`, `__pycache__/`, `.DS_Store`, prior bundle
files). To control this directly, add a `.bundlespec` file at the root
of the directory you're packing.

The syntax is `.gitignore`'s glob syntax, inverted. A
`.gitignore` excludes by default, so a bare line excludes and `!`
re-includes. A `.bundlespec` includes by default, so a bare line
**includes**, and `!` **excludes** something an earlier line already
matched:

```
# what to bundle
src/**
assets/**
Cargo.toml

# exceptions
!target/
!node_modules/
!*.log
!secrets.rs
```

Rules apply top to bottom; the last matching rule for a given path wins.
List broad includes first, then narrow with `!`.

## Command-line reference

```
[python3] utfbundle [src_dir] [-o bundle.py] [-s SPEC] [-p PATTERN ...] [-n NAME]
```

| Flag | Meaning |
|---|---|
| `src_dir` | Directory to pack. Defaults to `.` |
| `-o, --output` | Output bundle path. Defaults to `<name>_bundle.py` |
| `-s, --spec` | Path to a patterns file. Defaults to `<src_dir>/.bundlespec` if present |
| `-p, --pattern` | An extra pattern, may be repeated; applied after the spec file |
| `-n, --project-name` | Name used for the bundle file and default extract destination |

## Limitations

- **Binary files are supported, but not free.** Each file is read as
  UTF-8 text; if that fails, it's stored instead as base64 inside the
  bundle (and a note is printed to stderr) and decoded back to exact
  bytes on extract. Either way the round-trip is exact, but base64
  content isn't meant to be read by eye the way the text entries are,
  and it inflates that file's size by roughly a third.
- **No secret-scanning.** It bundles whatever the patterns match. Keep
  credentials and `.env` files out via `.bundlespec`, the same way you
  would for a commit.
- **No compression.** Bundle size is roughly the sum of the included
  files' sizes (plus the base64 overhead on any binary files).
- **Non-UTF-8 text is treated as binary.** A file in Latin-1,
  Windows-1252, UTF-16, etc. will fail the UTF-8 decode check and get
  stored as base64 rather than as a readable literal. It still
  round-trips exactly.  This only affects whether it's human-readable
  inside the bundle file.

## License

MIT
