pilepack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pilepack/__init__.py ADDED
File without changes
pilepack/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == '__main__':
5
+ main()
pilepack/cli.py ADDED
@@ -0,0 +1,103 @@
1
+ import argparse
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ from .collector import collect_files, build_tree
6
+ from .reader import read_file
7
+ from .formatter import render
8
+
9
+
10
+ def _generate_report(root_path: Path, include_content=True, mask_secrets=False, follow_gitignore=True, fmt="txt"):
11
+ print("Scanning files...", file=sys.stderr, flush=True)
12
+ files = collect_files(root_path, follow_gitignore=follow_gitignore)
13
+ tree = build_tree(files)
14
+
15
+ files_content = []
16
+ if include_content:
17
+ total = len(files)
18
+ print(f"Reading {total} files...", file=sys.stderr, flush=True)
19
+ for rel_path in files:
20
+ abs_path = root_path / rel_path
21
+ content = read_file(abs_path, mask_secrets=mask_secrets)
22
+ files_content.append((rel_path, content))
23
+ print(f"Done. Read {total} files.", file=sys.stderr, flush=True)
24
+ else:
25
+ files_content = []
26
+
27
+ root_name = root_path.name or str(root_path)
28
+ report = render(root_name, tree, files_content, fmt=fmt)
29
+ return report
30
+
31
+
32
+ def main():
33
+ parser = argparse.ArgumentParser(
34
+ description="Pack your codebase into a single file for AI analysis"
35
+ )
36
+ parser.add_argument(
37
+ "root",
38
+ nargs="?",
39
+ default=".",
40
+ help="Root directory to scan (default: current directory)",
41
+ )
42
+ parser.add_argument(
43
+ "--no-content",
44
+ dest="include_content",
45
+ action="store_false",
46
+ help="Do not include file contents (only show tree structure)"
47
+ )
48
+ parser.add_argument(
49
+ "--mask-secrets",
50
+ action="store_true",
51
+ help="Mask sensitive information like passwords, tokens, etc."
52
+ )
53
+ parser.add_argument(
54
+ "-o", "--output",
55
+ type=Path,
56
+ help="Write output to a file instead of stdout"
57
+ )
58
+ parser.add_argument(
59
+ "--no-gitignore",
60
+ dest="follow_gitignore",
61
+ action="store_false",
62
+ help="Do NOT respect .gitignore rules (include all files)"
63
+ )
64
+ parser.add_argument(
65
+ "-f", "--format",
66
+ choices=["txt", "md"],
67
+ default="txt",
68
+ help="Output format: txt (default) or md",
69
+ metavar='FMT'
70
+ )
71
+
72
+ args = parser.parse_args()
73
+
74
+ root_path = Path(args.root).resolve()
75
+ if not root_path.is_dir():
76
+ print(f"Error: '{root_path}' is not a valid directory.", file=sys.stderr)
77
+ sys.exit(1)
78
+
79
+ try:
80
+ report = _generate_report(
81
+ root_path,
82
+ include_content=args.include_content,
83
+ mask_secrets=args.mask_secrets,
84
+ follow_gitignore=args.follow_gitignore,
85
+ fmt=args.format,
86
+ )
87
+ except Exception as e:
88
+ print(f"Error generating report: {e}", file=sys.stderr)
89
+ sys.exit(1)
90
+
91
+ if args.output:
92
+ try:
93
+ args.output.write_text(report, encoding='utf-8')
94
+ print(f"Report written to {args.output}")
95
+ except IOError as e:
96
+ print(f"Error writing to file: {e}", file=sys.stderr)
97
+ sys.exit(1)
98
+ else:
99
+ print(report)
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
pilepack/collector.py ADDED
@@ -0,0 +1,37 @@
1
+ from .ignorer import load_gitignore, is_ignored
2
+ from pathlib import Path
3
+ from typing import Dict, List
4
+
5
+
6
+ def collect_files(root_path: Path, follow_gitignore: bool = True) -> list[Path]:
7
+ if not root_path.is_dir():
8
+ raise NotADirectoryError(f"{root_path} does not exist or is not a directory")
9
+
10
+ if follow_gitignore:
11
+ spec = load_gitignore(root_path)
12
+ else:
13
+ spec = None
14
+ collected = []
15
+
16
+ for item in root_path.rglob('*'):
17
+ if item.name == '.gitignore':
18
+ continue
19
+ if '.git' in item.parts:
20
+ continue
21
+ if spec and is_ignored(item, root_path, spec):
22
+ continue
23
+ if item.is_file():
24
+ rel = item.relative_to(root_path)
25
+ collected.append(rel)
26
+ return collected
27
+
28
+
29
+ def build_tree(files: List[Path]) -> Dict:
30
+ tree = {}
31
+ for path in files:
32
+ parts = path.parts
33
+ current = tree
34
+ for part in parts[:-1]:
35
+ current = current.setdefault(part, {})
36
+ current[parts[-1]] = None
37
+ return tree
pilepack/formatter.py ADDED
@@ -0,0 +1,68 @@
1
+ from typing import Dict, List, Optional, Tuple
2
+ from pathlib import Path
3
+
4
+
5
+ def _format_tree(tree: Dict, prefix: str = '', is_last: bool = True) -> str:
6
+ lines = []
7
+ items = sorted(tree.items(), key=lambda x: (isinstance(x[1], dict), x[0].lower()), reverse=True)
8
+
9
+ for i, (name, subtree) in enumerate(items):
10
+ is_last_item = (i == len(items) - 1)
11
+ connector = 'โ””โ”€โ”€ ' if is_last_item else 'โ”œโ”€โ”€ '
12
+ display_name = name + '/' if isinstance(subtree, dict) else name
13
+ lines.append(prefix + connector + display_name)
14
+
15
+ if isinstance(subtree, dict):
16
+ new_prefix = prefix + (' ' if is_last_item else 'โ”‚ ')
17
+ lines.append(_format_tree(subtree, new_prefix, is_last_item))
18
+ return '\n'.join(lines)
19
+
20
+
21
+ def _render_txt(root_name: str, tree: Dict, files_content: List[Tuple[Path, Optional[str]]]) -> str:
22
+ tree_str = _format_tree(tree)
23
+ output = [f"{root_name}\n{tree_str}"]
24
+
25
+ if files_content:
26
+ output.append("\n" + "=" * 80 + "\n")
27
+
28
+ for rel_path, content in files_content:
29
+
30
+ if content is not None:
31
+ output.append(f"--- FILE: {rel_path} ---")
32
+ output.append(content)
33
+ output.append("")
34
+ else:
35
+ output.append(f"--- FILE: {rel_path} [BINARY OR UNREADABLE] ---\n")
36
+ return "\n".join(output)
37
+
38
+
39
+ def _render_md(root_name: str, tree: Dict, files_content: List[Tuple[Path, Optional[str]]]) -> str:
40
+ tree_str = _format_tree(tree)
41
+ output = [f"# {root_name}\n```\n{tree_str}\n```"]
42
+
43
+ if files_content:
44
+ output.append("\n---\n")
45
+
46
+ for rel_path, content in files_content:
47
+
48
+ if content is not None:
49
+ ext = rel_path.suffix.lower()
50
+ lang = {
51
+ '.py': 'python', '.js': 'javascript', '.ts': 'typescript',
52
+ '.html': 'html', '.css': 'css', '.json': 'json',
53
+ '.md': 'markdown', '.yaml': 'yaml', '.yml': 'yaml',
54
+ '.sh': 'bash', '.txt': 'text'
55
+ }.get(ext, 'text')
56
+ output.append(f"## `{rel_path}`\n```{lang}\n{content}\n```\n")
57
+ else:
58
+ output.append(f"## `{rel_path}`\n*[BINARY OR UNREADABLE]*\n")
59
+ return '\n'.join(output)
60
+
61
+
62
+ def render(root_name: str, tree: Dict, files_content: List[Tuple[Path, Optional[str]]], fmt: str = "txt") -> str:
63
+ if fmt == "txt":
64
+ return _render_txt(root_name, tree, files_content)
65
+ elif fmt == "md":
66
+ return _render_md(root_name, tree, files_content)
67
+ else:
68
+ raise ValueError(f"Unsupported format: {fmt}. Supported: 'txt', 'md'")
pilepack/ignorer.py ADDED
@@ -0,0 +1,28 @@
1
+ from pathlib import Path
2
+ from pathspec import PathSpec
3
+
4
+
5
+ def load_gitignore(root_path: Path) -> PathSpec:
6
+ gitignore_path = root_path / '.gitignore'
7
+ if not gitignore_path.exists():
8
+ return PathSpec.from_lines('gitignore', [])
9
+ with open(gitignore_path, 'r', encoding='utf-8') as file:
10
+ return PathSpec.from_lines('gitignore', file)
11
+
12
+
13
+ def is_ignored(path, root: Path, spec: PathSpec) -> bool:
14
+ original = str(path)
15
+ path_obj = Path(original)
16
+
17
+ if not path_obj.is_absolute():
18
+ path_obj = root / path_obj
19
+
20
+ try:
21
+ rel_path = path_obj.relative_to(root)
22
+ except ValueError:
23
+ return False
24
+ rel_str = rel_path.as_posix()
25
+
26
+ if original.endswith(('/', '\\')) or (path_obj.exists() and path_obj.is_dir()):
27
+ rel_str += '/'
28
+ return spec.match_file(rel_str)
pilepack/reader.py ADDED
@@ -0,0 +1,46 @@
1
+ import re
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ SECRET_PATTERNS = [
6
+ (r'(password|passwd|pwd)\s*[=:]\s*["\']?([^"\'\s]+)["\']?', r'\1="***"'),
7
+ (r'(api_key|apikey)\s*[=:]\s*["\']?([^"\'\s]+)["\']?', r'\1="***"'),
8
+ (r'(token|access_token)\s*[=:]\s*["\']?([^"\'\s]+)["\']?', r'\1="***"'),
9
+ (r'(secret|private_key)\s*[=:]\s*["\']?([^"\'\s]+)["\']?', r'\1="***"'),
10
+ (r'(?:\\b[A-Za-z0-9+/]{40,}\\b)', '***'),
11
+ (r'(?:\\b[0-9a-f]{32,}\\b)', '***')
12
+ ]
13
+
14
+ def _mask_secrets_in_text(text: str) -> str:
15
+ for pattern, replacement in SECRET_PATTERNS:
16
+ text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
17
+ return text
18
+
19
+ def read_file(file_path: Path, mask_secrets: bool = False) -> Optional[str]:
20
+ try:
21
+ raw_data = file_path.read_bytes()
22
+ except (OSError, IOError) as e:
23
+ print(f"Failed to read {file_path}: {e}")
24
+ return None
25
+
26
+ if b'\x00' in raw_data:
27
+ return None
28
+ try:
29
+ text = raw_data.decode('utf-8-sig')
30
+ except UnicodeDecodeError:
31
+ for enc in ('utf-8', 'cp1251', 'latin1'):
32
+ try:
33
+ text = raw_data.decode(enc)
34
+ break
35
+ except UnicodeDecodeError:
36
+ continue
37
+ else:
38
+ return None
39
+
40
+ if text.startswith('\ufeff'):
41
+ text = text[1:]
42
+
43
+ if mask_secrets:
44
+ text = _mask_secrets_in_text(text)
45
+
46
+ return text
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: pilepack
3
+ Version: 0.1.0
4
+ Summary: Pack your codebase into a single file for AI analysis
5
+ Author-email: "Vasili S. Pribylov" <dartmew@yandex.com>
6
+ License: MIT
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: pathspec>=0.10.0
11
+ Provides-Extra: test
12
+ Requires-Dist: pytest>=7.0; extra == "test"
13
+ Requires-Dist: pytest-cov; extra == "test"
14
+ Dynamic: license-file
15
+
16
+ # PilePack
17
+
18
+ [![Python Version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
19
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
20
+ [![PyPI version](https://img.shields.io/pypi/v/pilepack)](https://pypi.org/project/pilepack/)
21
+ [![Tests](https://img.shields.io/badge/tests-20%20passed-brightgreen)](tests/)
22
+ [![Coverage](https://img.shields.io/badge/coverage-84%25-yellowgreen)](tests/)
23
+ [![CLI](https://img.shields.io/badge/CLI-ready-blue)]()
24
+
25
+ **Pack your codebase into a single file for AI analysis**
26
+ Combine all your project files into one text file โ€” perfect for sending to LLMs (ChatGPT, Claude, Copilot, Deepseek, etc.).
27
+
28
+ ---
29
+
30
+ ## โœจ Features
31
+
32
+ - ๐Ÿ“ **Recursive scanning** โ€“ walks through all files in a directory.
33
+ - ๐Ÿšซ **Respects .gitignore** โ€“ optionally disable with `--no-gitignore`.
34
+ - ๐ŸŒณ **Tree structure** โ€“ displays project hierarchy.
35
+ - ๐Ÿ“„ **Embedded content** โ€“ each file is shown with its path header.
36
+ - ๐Ÿ” **Secrets masking** โ€“ hides passwords, tokens, keys (`--mask-secrets`).
37
+ - ๐Ÿ–จ๏ธ **Two output formats** โ€“ plain text (`txt`) or Markdown (`md`).
38
+ - ๐Ÿ’พ **Save to file** โ€“ use `-o output.txt`.
39
+
40
+ ---
41
+
42
+ ## ๐Ÿ“ฆ Installation
43
+
44
+ ```bash
45
+ pip install pilepack
46
+ ```
47
+ From source:
48
+
49
+ ```bash
50
+ git clone https://github.com/dartmew/pilepack.git
51
+ cd pilepack
52
+ pip install -e .
53
+ ```
54
+
55
+ ## ๐Ÿš€ Usage
56
+ Basic command โ€“ pass a path to your project:
57
+ ```bash
58
+ pilepack /path/to/your/project
59
+ ```
60
+ Redirect output to a file:
61
+ ```bash
62
+ pilepack . > report.txt
63
+ ```
64
+ Example output (txt)
65
+ ```text
66
+ myproject
67
+ โ”œโ”€โ”€ main.py
68
+ โ”œโ”€โ”€ utils/
69
+ โ”‚ โ”œโ”€โ”€ helpers.py
70
+ โ”‚ โ””โ”€โ”€ __init__.py
71
+ โ””โ”€โ”€ README.md
72
+
73
+ ================================================================================
74
+
75
+ --- FILE: main.py ---
76
+ import utils.helpers
77
+
78
+ def main():
79
+ print("Hello")
80
+
81
+ --- FILE: utils/helpers.py ---
82
+ def greet(name):
83
+ return f"Hi {name}"
84
+ ```
85
+ Markdown format
86
+ ```bash
87
+ pilepack . -f md -o report.md
88
+ ```
89
+ Produces a Markdown file with syntax highlighting.
90
+
91
+ Show only structure (no file contents)
92
+ ```bash
93
+ pilepack . --no-content
94
+ ```
95
+ Mask secrets
96
+ ```bash
97
+ pilepack . --mask-secrets
98
+ ```
99
+ Replaces values of password=, api_key=, token=, and long strings (base64/hex) with ***.
100
+
101
+ Disable .gitignore
102
+ ```bash
103
+ pilepack . --no-gitignore
104
+ ```
105
+ ## ๐Ÿ“‹ CLI Options
106
+ | Option | Description |
107
+ |--------|-------------|
108
+ | `root` | Directory to scan (default: current directory) |
109
+ | `--no-content` | Show tree structure only, skip file contents |
110
+ | `--mask-secrets` | Mask passwords, tokens, API keys |
111
+ | `-o, --output` | Write report to a file instead of stdout |
112
+ | `--no-gitignore` | Do not respect `.gitignore` (include all files) |
113
+ | `-f, --format` | Output format: `txt` (default) or `md` |
114
+
115
+ ## ๐Ÿงช Testing
116
+ Install test dependencies and run:
117
+
118
+ ```bash
119
+ pip install -e .[test]
120
+ pytest
121
+ ```
122
+ With coverage:
123
+ ```bash
124
+ pytest --cov=pilepack
125
+ ```
126
+ Current coverage: 84% (20 tests, all passing).
127
+ ```bash
128
+ Name Stmts Miss Cover
129
+ -------------------------------------------
130
+ pilepack\__init__.py 0 0 100%
131
+ pilepack\__main__.py 3 3 0%
132
+ pilepack\cli.py 51 7 86%
133
+ pilepack\collector.py 30 1 97%
134
+ pilepack\formatter.py 44 3 93%
135
+ pilepack\ignorer.py 21 3 86%
136
+ pilepack\reader.py 31 12 61%
137
+ -------------------------------------------
138
+ TOTAL 180 29 84%
139
+ ```
140
+
141
+ ## ๐Ÿ“„ License
142
+ [MIT](LICENSE) ยฉ 2026 Vasili S. Pribylov
143
+
144
+ ## ๐Ÿค Contributing
145
+ Issues and pull requests are welcome! For major changes, please open an issue first to discuss.
146
+
147
+ ## ๐Ÿ™ Acknowledgements
148
+ Inspired by the need to easily feed code into large language models.
@@ -0,0 +1,13 @@
1
+ pilepack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pilepack/__main__.py,sha256=hgA7I86RlZPQ28yoxwUz5uOhsxK8fzYfaJ6XYkq2nQ4,65
3
+ pilepack/cli.py,sha256=nRrDwnU6D0KnwEOO_k2rnb7HqL0T0CzFsGHy2-yfYuI,3174
4
+ pilepack/collector.py,sha256=3uMoW68mjIFF-eaa3qUAS7CcSd1RMWX_VFAOTjPOdnM,1077
5
+ pilepack/formatter.py,sha256=sBv312kZjz-3etlIUOqxzj0698LTLMLZZbHrhMrRK84,2723
6
+ pilepack/ignorer.py,sha256=PMzl_Dx8gWUYxQHvccIBw-OAdYPn-ihbFcmTzYGkXpY,848
7
+ pilepack/reader.py,sha256=QEUPmCj8h_Po4Zzt0Yz6qdRyfBdK8-m6hxTzf0j19D4,1447
8
+ pilepack-0.1.0.dist-info/licenses/LICENSE,sha256=U6IZ7mLlAQFo7cMcX0eqWuPtgiP2RRAWfnYdfVmEc30,1079
9
+ pilepack-0.1.0.dist-info/METADATA,sha256=97DTfm7WtQuOSbFEtYgJznhjcEjqH7xB4lbNitv2Cd4,4282
10
+ pilepack-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ pilepack-0.1.0.dist-info/entry_points.txt,sha256=ywLekmCJYvrm_uymq5cWDu3GJXZVDUULh-q14QNRLN4,47
12
+ pilepack-0.1.0.dist-info/top_level.txt,sha256=PBbRF3bR-s_sO1hbRtLORxUipnbLHUytM7Lh6ecgbuI,9
13
+ pilepack-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pilepack = pilepack.cli:main
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 Vasili S. Pribylov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ pilepack