py-for-ai 0.1.0__tar.gz
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.
- py_for_ai-0.1.0/.gitignore +3 -0
- py_for_ai-0.1.0/LICENSE +21 -0
- py_for_ai-0.1.0/PKG-INFO +136 -0
- py_for_ai-0.1.0/README.md +119 -0
- py_for_ai-0.1.0/pyproject.toml +34 -0
- py_for_ai-0.1.0/src/py_ai/__init__.py +6 -0
- py_for_ai-0.1.0/src/py_ai/__main__.py +7 -0
- py_for_ai-0.1.0/src/py_ai/cli.py +89 -0
- py_for_ai-0.1.0/src/py_ai/core.py +264 -0
py_for_ai-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maksym Khlystun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
py_for_ai-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-for-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A command-line utility to pack Python codebases into a single text file for AI context, with clipboard support.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Maksum867/py-ai
|
|
6
|
+
Author: Maksym Khlystun
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development
|
|
13
|
+
Classifier: Topic :: Utilities
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Requires-Dist: pyperclip>=1.8.2
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
<div align="center">
|
|
19
|
+
|
|
20
|
+
# py-ai π
|
|
21
|
+
|
|
22
|
+
[](https://pypi.org/project/py-ai-pack/)
|
|
23
|
+
[](https://github.com/Maksum867/py-ai/blob/main/LICENSE)
|
|
24
|
+
[](https://pypi.org/project/py-ai-pack/)
|
|
25
|
+
|
|
26
|
+
`py-ai` is a lightweight, zero-configuration command-line interface (CLI) tool designed for developers. It recursively scans your local Python codebase, filters out unnecessary files, and compiles your entire projectβcomplete with a beautiful ASCII directory treeβinto a single, organized text file while automatically copying it to your clipboard.
|
|
27
|
+
|
|
28
|
+
Perfect for instantly feeding your codebase context into Large Language Models (LLMs) like ChatGPT, Claude, and Gemini.
|
|
29
|
+
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## β¨ Features
|
|
35
|
+
|
|
36
|
+
- **Smart Filtering**: Automatically ignores VCS folders (`.git`), virtual environments (`.venv`, `venv`), IDE settings (`.vscode`, `.idea`), build artifacts (`dist`, `build`), and binary files (images, archives, databases).
|
|
37
|
+
- **ASCII Directory Tree**: Generates a clean, sorted representation of your project layout at the top of the output.
|
|
38
|
+
- **Normalized File Markers**: Wraps each file's code in distinct delimiters (`--- START OF FILE: ... ---`) using normalized cross-platform POSIX paths (`/`).
|
|
39
|
+
- **Clipboard Integration**: Automatically copies the packed content. Gracefully falls back with a warning in headless/SSH environments rather than crashing.
|
|
40
|
+
- **PyPI & Hatchling Ready**: Out-of-the-box support for modern PEP 621 packaging.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## π Project Layout
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
py_ai_project/
|
|
48
|
+
βββ pyproject.toml
|
|
49
|
+
βββ README.md
|
|
50
|
+
βββ src/
|
|
51
|
+
βββ py_ai/
|
|
52
|
+
βββ __init__.py
|
|
53
|
+
βββ __main__.py
|
|
54
|
+
βββ core.py
|
|
55
|
+
βββ cli.py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## π Quick Start
|
|
61
|
+
|
|
62
|
+
### Installation
|
|
63
|
+
|
|
64
|
+
Install the utility directly from PyPI:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install py-ai
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
*(Or install it locally for development)*:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/Maksum867/py-ai.git
|
|
74
|
+
cd py_ai_project
|
|
75
|
+
pip install -e .
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Usage
|
|
79
|
+
|
|
80
|
+
Run the utility from any terminal session:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 1. Pack current directory and copy to clipboard:
|
|
84
|
+
pyai
|
|
85
|
+
|
|
86
|
+
# 2. Pack a specific directory:
|
|
87
|
+
pyai /path/to/your/project
|
|
88
|
+
|
|
89
|
+
# 3. Save the results to a custom file:
|
|
90
|
+
pyai . -o custom_output.txt
|
|
91
|
+
|
|
92
|
+
# 4. Run without clipboard copy (ideal for remote servers or CI/CD):
|
|
93
|
+
pyai --no-clipboard
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## π Output Format Example
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
================================================================================
|
|
102
|
+
PROJECT CONTEXT PACK: my_project
|
|
103
|
+
Generated on: 2026-07-31 12:00:00
|
|
104
|
+
Total files packed: 2
|
|
105
|
+
================================================================================
|
|
106
|
+
|
|
107
|
+
================================================================================
|
|
108
|
+
DIRECTORY TREE
|
|
109
|
+
================================================================================
|
|
110
|
+
my_project/
|
|
111
|
+
βββ src/
|
|
112
|
+
β βββ main.py
|
|
113
|
+
βββ pyproject.toml
|
|
114
|
+
|
|
115
|
+
================================================================================
|
|
116
|
+
FILES CONTENT
|
|
117
|
+
================================================================================
|
|
118
|
+
|
|
119
|
+
--- START OF FILE: pyproject.toml ---
|
|
120
|
+
[project]
|
|
121
|
+
name = "my_project"
|
|
122
|
+
version = "0.1.0"
|
|
123
|
+
--- END OF FILE: pyproject.toml ---
|
|
124
|
+
|
|
125
|
+
--- START OF FILE: src/main.py ---
|
|
126
|
+
def main():
|
|
127
|
+
print("Hello, AI!")
|
|
128
|
+
--- END OF FILE: src/main.py ---
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## π οΈ Requirements & Dependencies
|
|
134
|
+
|
|
135
|
+
- Python `3.8` or higher.
|
|
136
|
+
- `pyperclip` (for clipboard operations).
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# py-ai π
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/py-ai-pack/)
|
|
6
|
+
[](https://github.com/Maksum867/py-ai/blob/main/LICENSE)
|
|
7
|
+
[](https://pypi.org/project/py-ai-pack/)
|
|
8
|
+
|
|
9
|
+
`py-ai` is a lightweight, zero-configuration command-line interface (CLI) tool designed for developers. It recursively scans your local Python codebase, filters out unnecessary files, and compiles your entire projectβcomplete with a beautiful ASCII directory treeβinto a single, organized text file while automatically copying it to your clipboard.
|
|
10
|
+
|
|
11
|
+
Perfect for instantly feeding your codebase context into Large Language Models (LLMs) like ChatGPT, Claude, and Gemini.
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## β¨ Features
|
|
18
|
+
|
|
19
|
+
- **Smart Filtering**: Automatically ignores VCS folders (`.git`), virtual environments (`.venv`, `venv`), IDE settings (`.vscode`, `.idea`), build artifacts (`dist`, `build`), and binary files (images, archives, databases).
|
|
20
|
+
- **ASCII Directory Tree**: Generates a clean, sorted representation of your project layout at the top of the output.
|
|
21
|
+
- **Normalized File Markers**: Wraps each file's code in distinct delimiters (`--- START OF FILE: ... ---`) using normalized cross-platform POSIX paths (`/`).
|
|
22
|
+
- **Clipboard Integration**: Automatically copies the packed content. Gracefully falls back with a warning in headless/SSH environments rather than crashing.
|
|
23
|
+
- **PyPI & Hatchling Ready**: Out-of-the-box support for modern PEP 621 packaging.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## π Project Layout
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
py_ai_project/
|
|
31
|
+
βββ pyproject.toml
|
|
32
|
+
βββ README.md
|
|
33
|
+
βββ src/
|
|
34
|
+
βββ py_ai/
|
|
35
|
+
βββ __init__.py
|
|
36
|
+
βββ __main__.py
|
|
37
|
+
βββ core.py
|
|
38
|
+
βββ cli.py
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## π Quick Start
|
|
44
|
+
|
|
45
|
+
### Installation
|
|
46
|
+
|
|
47
|
+
Install the utility directly from PyPI:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install py-ai
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
*(Or install it locally for development)*:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
git clone https://github.com/Maksum867/py-ai.git
|
|
57
|
+
cd py_ai_project
|
|
58
|
+
pip install -e .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Usage
|
|
62
|
+
|
|
63
|
+
Run the utility from any terminal session:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# 1. Pack current directory and copy to clipboard:
|
|
67
|
+
pyai
|
|
68
|
+
|
|
69
|
+
# 2. Pack a specific directory:
|
|
70
|
+
pyai /path/to/your/project
|
|
71
|
+
|
|
72
|
+
# 3. Save the results to a custom file:
|
|
73
|
+
pyai . -o custom_output.txt
|
|
74
|
+
|
|
75
|
+
# 4. Run without clipboard copy (ideal for remote servers or CI/CD):
|
|
76
|
+
pyai --no-clipboard
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## π Output Format Example
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
================================================================================
|
|
85
|
+
PROJECT CONTEXT PACK: my_project
|
|
86
|
+
Generated on: 2026-07-31 12:00:00
|
|
87
|
+
Total files packed: 2
|
|
88
|
+
================================================================================
|
|
89
|
+
|
|
90
|
+
================================================================================
|
|
91
|
+
DIRECTORY TREE
|
|
92
|
+
================================================================================
|
|
93
|
+
my_project/
|
|
94
|
+
βββ src/
|
|
95
|
+
β βββ main.py
|
|
96
|
+
βββ pyproject.toml
|
|
97
|
+
|
|
98
|
+
================================================================================
|
|
99
|
+
FILES CONTENT
|
|
100
|
+
================================================================================
|
|
101
|
+
|
|
102
|
+
--- START OF FILE: pyproject.toml ---
|
|
103
|
+
[project]
|
|
104
|
+
name = "my_project"
|
|
105
|
+
version = "0.1.0"
|
|
106
|
+
--- END OF FILE: pyproject.toml ---
|
|
107
|
+
|
|
108
|
+
--- START OF FILE: src/main.py ---
|
|
109
|
+
def main():
|
|
110
|
+
print("Hello, AI!")
|
|
111
|
+
--- END OF FILE: src/main.py ---
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## π οΈ Requirements & Dependencies
|
|
117
|
+
|
|
118
|
+
- Python `3.8` or higher.
|
|
119
|
+
- `pyperclip` (for clipboard operations).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "py-for-ai"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A command-line utility to pack Python codebases into a single text file for AI context, with clipboard support."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Maksym Khlystun"}
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Topic :: Software Development",
|
|
20
|
+
"Topic :: Utilities",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"pyperclip>=1.8.2",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/Maksum867/py-ai"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
pyai = "py_ai.cli:main"
|
|
31
|
+
py-ai = "py_ai.cli:main"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/py_ai"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli.py module
|
|
3
|
+
=============
|
|
4
|
+
Command-line interface (CLI) for the py-ai utility using the built-in argparse.
|
|
5
|
+
Provides user interaction, argument parsing, and triggers the packing logic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from py_ai.core import pack_project
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
"""
|
|
16
|
+
Main entry point for the command-line interface. Parses CLI arguments,
|
|
17
|
+
starts the project packaging process, and prints results to the console.
|
|
18
|
+
"""
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
description="py-ai: A command-line utility to pack your project's codebase into a single text file and copy it to the clipboard for LLM context."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"root_dir",
|
|
25
|
+
nargs="?",
|
|
26
|
+
default=".",
|
|
27
|
+
help="Path to the project directory to pack (default: current directory '.')"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"-o", "--output",
|
|
32
|
+
default="ai_context.txt",
|
|
33
|
+
help="Name or path of the output file (default: 'ai_context.txt')"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--no-clipboard",
|
|
38
|
+
action="store_true",
|
|
39
|
+
help="Disable automatic copying of the generated context to the system clipboard (useful for headless servers or CI/CD pipelines)."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
|
|
44
|
+
root_path = Path(args.root_dir)
|
|
45
|
+
output_path = Path(args.output)
|
|
46
|
+
|
|
47
|
+
print(f"π Scanning project directory: {root_path.resolve()}")
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# Trigger the core packing logic
|
|
51
|
+
stats = pack_project(
|
|
52
|
+
root_dir=root_path,
|
|
53
|
+
output_file=output_path,
|
|
54
|
+
copy_to_clipboard=not args.no_clipboard
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
print("\n⨠Project successfully packed!")
|
|
58
|
+
print(f"π Root Directory: {stats['root_dir']}")
|
|
59
|
+
print(f"π Output File: {stats['output_file']}")
|
|
60
|
+
print(f"π¦ Packed Files: {stats['packed_count']}")
|
|
61
|
+
|
|
62
|
+
if stats['failed_count'] > 0:
|
|
63
|
+
print(f"β οΈ Skipped Files: {stats['failed_count']} (see warning messages above)")
|
|
64
|
+
|
|
65
|
+
if stats['clipboard_copied']:
|
|
66
|
+
print("π Project context copied to clipboard successfully!")
|
|
67
|
+
else:
|
|
68
|
+
if args.no_clipboard:
|
|
69
|
+
print("βΉοΈ Clipboard copying was disabled by the --no-clipboard flag.")
|
|
70
|
+
else:
|
|
71
|
+
print(
|
|
72
|
+
f"β οΈ Could not copy to clipboard.\n"
|
|
73
|
+
f" Details: {stats['clipboard_error'] or 'Unknown error'}.\n"
|
|
74
|
+
f" The output was still saved successfully to the output file."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
except FileNotFoundError as e:
|
|
78
|
+
print(f"β Error: {e}", file=sys.stderr)
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
except IOError as e:
|
|
81
|
+
print(f"β Write Error: {e}", file=sys.stderr)
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"β Unexpected Error: {e}", file=sys.stderr)
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
main()
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
core.py module
|
|
3
|
+
==============
|
|
4
|
+
Contains the core business logic for the py-ai utility:
|
|
5
|
+
- File and directory ignore checks (should_ignore).
|
|
6
|
+
- Project directory tree generation in ASCII format.
|
|
7
|
+
- Gathering allowed file contents and writing them to the final context file.
|
|
8
|
+
- Copying the generated output to the system clipboard.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
import pyperclip
|
|
16
|
+
|
|
17
|
+
# Standard lists of exclusions for ignore checks
|
|
18
|
+
IGNORED_NAMES = {
|
|
19
|
+
# Version control and system folders
|
|
20
|
+
'.git', '.github', '.gitlab', '.svn', '.hg', 'node_modules',
|
|
21
|
+
# Python runtime and caches
|
|
22
|
+
'__pycache__', '.venv', 'venv', 'env', '.env', '.pytest_cache',
|
|
23
|
+
'.mypy_cache', '.ruff_cache', '.tox', '.nox', '.egg-info', 'build', 'dist',
|
|
24
|
+
# IDE and editor settings
|
|
25
|
+
'.idea', '.vscode', '.settings',
|
|
26
|
+
# OS system files
|
|
27
|
+
'.DS_Store', 'Thumbs.db', 'desktop.ini'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
IGNORED_EXTENSIONS = {
|
|
31
|
+
# Compiled Python and binary artifacts
|
|
32
|
+
'.pyc', '.pyo', '.pyd', '.class', '.o', '.obj', '.dll', '.so', '.dylib',
|
|
33
|
+
# Archives/Compressed files
|
|
34
|
+
'.zip', '.tar', '.gz', '.bz2', '.xz', '.rar', '.7z', '.tgz',
|
|
35
|
+
# Images and multimedia
|
|
36
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.tiff', '.svg',
|
|
37
|
+
'.mp3', '.wav', '.ogg', '.flac', '.mp4', '.mkv', '.avi', '.mov', '.webm',
|
|
38
|
+
# Fonts
|
|
39
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
40
|
+
# Databases and binary documents
|
|
41
|
+
'.db', '.sqlite', '.sqlite3', '.pdf', '.epub',
|
|
42
|
+
# Office documents
|
|
43
|
+
'.docx', '.xlsx', '.pptx', '.doc', '.xls', '.ppt'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def should_ignore(path: Path, root_dir: Path) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Checks if a file or directory should be ignored based on standard exclusions.
|
|
50
|
+
|
|
51
|
+
:param path: Path to the file or folder to verify.
|
|
52
|
+
:param root_dir: Root directory of the project.
|
|
53
|
+
:return: True if the path should be ignored, False otherwise.
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
# Resolve to absolute paths to avoid issues with relative path operations
|
|
57
|
+
abs_root = root_dir.resolve()
|
|
58
|
+
abs_path = path.resolve()
|
|
59
|
+
# Calculate path relative to the root directory
|
|
60
|
+
rel_path = abs_path.relative_to(abs_root)
|
|
61
|
+
except ValueError:
|
|
62
|
+
# Fallback if the path is not under the root directory
|
|
63
|
+
rel_path = path
|
|
64
|
+
|
|
65
|
+
# Check each part of the relative path.
|
|
66
|
+
# If any parent folder is in the ignored list, ignore the entire subtree.
|
|
67
|
+
for part in rel_path.parts:
|
|
68
|
+
if part in IGNORED_NAMES or part.lower() in IGNORED_NAMES:
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
# Ignore any hidden folders/files (starting with a dot),
|
|
72
|
+
# except allowed configuration files such as .gitignore or .env.example
|
|
73
|
+
if part.startswith('.') and part not in ('.gitignore', '.env.example', '.env.template', '.pylintrc'):
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
# Additional file-level checks for extensions and file names
|
|
77
|
+
if path.is_file():
|
|
78
|
+
if path.suffix.lower() in IGNORED_EXTENSIONS:
|
|
79
|
+
return True
|
|
80
|
+
if path.name.lower() in IGNORED_NAMES:
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_tree_lines(path: Path, root_dir: Path, prefix: str = "", is_last: bool = True, is_root: bool = False) -> list[str]:
|
|
87
|
+
"""
|
|
88
|
+
Recursively generates the ASCII project tree, skipping ignored files and folders.
|
|
89
|
+
|
|
90
|
+
:param path: Current path (file or folder).
|
|
91
|
+
:param root_dir: Root directory of the project (for should_ignore checks).
|
|
92
|
+
:param prefix: Prefix for the current line (indentation and connection symbols).
|
|
93
|
+
:param is_last: Whether the current item is the last one in its parent directory.
|
|
94
|
+
:param is_root: Whether the current item is the root folder of the project.
|
|
95
|
+
:return: List of strings forming the ASCII tree.
|
|
96
|
+
"""
|
|
97
|
+
lines = []
|
|
98
|
+
|
|
99
|
+
# Root directory displays with its name and a trailing slash
|
|
100
|
+
if is_root:
|
|
101
|
+
lines.append(f"{path.name}/")
|
|
102
|
+
else:
|
|
103
|
+
connector = "βββ " if is_last else "βββ "
|
|
104
|
+
display_name = path.name + ("/" if path.is_dir() else "")
|
|
105
|
+
lines.append(f"{prefix}{connector}{display_name}")
|
|
106
|
+
|
|
107
|
+
if path.is_dir():
|
|
108
|
+
try:
|
|
109
|
+
# List directory items, sort them: directories first, then files (case-insensitive)
|
|
110
|
+
# Filter out any ignored items immediately
|
|
111
|
+
items = sorted(
|
|
112
|
+
[item for item in path.iterdir() if not should_ignore(item, root_dir)],
|
|
113
|
+
key=lambda x: (not x.is_dir(), x.name.lower())
|
|
114
|
+
)
|
|
115
|
+
except OSError as e:
|
|
116
|
+
# Handle permission errors or system issues gracefully in the tree
|
|
117
|
+
connector = "βββ " if is_last else "βββ "
|
|
118
|
+
lines.append(f"{prefix}{connector}[Permission denied/Error accessing directory: {e}]")
|
|
119
|
+
return lines
|
|
120
|
+
|
|
121
|
+
for i, item in enumerate(items):
|
|
122
|
+
item_is_last = (i == len(items) - 1)
|
|
123
|
+
# Calculate next indentation prefix for child elements
|
|
124
|
+
if is_root:
|
|
125
|
+
next_prefix = ""
|
|
126
|
+
else:
|
|
127
|
+
next_prefix = prefix + (" " if is_last else "β ")
|
|
128
|
+
|
|
129
|
+
lines.extend(build_tree_lines(item, root_dir, next_prefix, item_is_last, is_root=False))
|
|
130
|
+
|
|
131
|
+
return lines
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def pack_project(root_dir: str | Path, output_file: str | Path, copy_to_clipboard: bool = True) -> dict:
|
|
135
|
+
"""
|
|
136
|
+
Recursively scans the project, builds an ASCII tree, gathers all text file contents,
|
|
137
|
+
saves the aggregated output to a single file, and copies it to the system clipboard.
|
|
138
|
+
|
|
139
|
+
:param root_dir: Path to the root folder of the project.
|
|
140
|
+
:param output_file: Path to the target output text file.
|
|
141
|
+
:param copy_to_clipboard: Whether to copy the generated context to the clipboard.
|
|
142
|
+
:return: Dictionary containing execution statistics.
|
|
143
|
+
"""
|
|
144
|
+
root_path = Path(root_dir).resolve()
|
|
145
|
+
output_path = Path(output_file).resolve()
|
|
146
|
+
|
|
147
|
+
if not root_path.exists() or not root_path.is_dir():
|
|
148
|
+
raise FileNotFoundError(f"The specified directory '{root_path}' does not exist or is not a directory.")
|
|
149
|
+
|
|
150
|
+
# 1. Generate the ASCII directory tree
|
|
151
|
+
tree_lines = build_tree_lines(root_path, root_path, is_root=True)
|
|
152
|
+
tree_text = "\n".join(tree_lines)
|
|
153
|
+
|
|
154
|
+
# 2. Collect all allowed files for code packing
|
|
155
|
+
files_to_pack = []
|
|
156
|
+
|
|
157
|
+
def recursive_walk(current_dir: Path):
|
|
158
|
+
try:
|
|
159
|
+
# Sort items for a deterministic walking order
|
|
160
|
+
items = sorted(current_dir.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
|
|
161
|
+
except PermissionError as e:
|
|
162
|
+
print(f"Warning: Permission denied reading directory {current_dir}: {e}", file=sys.stderr)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
for item in items:
|
|
166
|
+
if should_ignore(item, root_path):
|
|
167
|
+
continue
|
|
168
|
+
if item.is_dir():
|
|
169
|
+
recursive_walk(item)
|
|
170
|
+
elif item.is_file():
|
|
171
|
+
# Avoid packing the output file itself if it is located inside the root project directory
|
|
172
|
+
if item.resolve() == output_path:
|
|
173
|
+
continue
|
|
174
|
+
files_to_pack.append(item)
|
|
175
|
+
|
|
176
|
+
recursive_walk(root_path)
|
|
177
|
+
|
|
178
|
+
# 3. Read file contents and format blocks
|
|
179
|
+
content_blocks = []
|
|
180
|
+
packed_count = 0
|
|
181
|
+
failed_count = 0
|
|
182
|
+
|
|
183
|
+
for file_path in files_to_pack:
|
|
184
|
+
try:
|
|
185
|
+
rel_path = file_path.relative_to(root_path).as_posix()
|
|
186
|
+
except ValueError:
|
|
187
|
+
rel_path = file_path.as_posix() if hasattr(file_path, "as_posix") else str(file_path)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
# Read files using UTF-8 encoding
|
|
191
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
192
|
+
content = f.read()
|
|
193
|
+
|
|
194
|
+
# Format the code block with clean delimiters
|
|
195
|
+
block = f"--- START OF FILE: {rel_path} ---\n{content}\n--- END OF FILE: {rel_path} ---"
|
|
196
|
+
content_blocks.append(block)
|
|
197
|
+
packed_count += 1
|
|
198
|
+
except UnicodeDecodeError:
|
|
199
|
+
# Handle binary files that cannot be decoded as standard text
|
|
200
|
+
print(f"Warning: Skipped '{rel_path}' due to encoding error (likely a binary file).", file=sys.stderr)
|
|
201
|
+
failed_count += 1
|
|
202
|
+
except PermissionError:
|
|
203
|
+
print(f"Warning: Skipped '{rel_path}' due to permission error.", file=sys.stderr)
|
|
204
|
+
failed_count += 1
|
|
205
|
+
except Exception as e:
|
|
206
|
+
print(f"Warning: Skipped '{rel_path}' due to an error: {e}", file=sys.stderr)
|
|
207
|
+
failed_count += 1
|
|
208
|
+
|
|
209
|
+
# 4. Formulate the single aggregated output
|
|
210
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
211
|
+
|
|
212
|
+
header = (
|
|
213
|
+
"================================================================================\n"
|
|
214
|
+
f"PROJECT CONTEXT PACK: {root_path.name}\n"
|
|
215
|
+
f"Generated on: {timestamp}\n"
|
|
216
|
+
f"Total files packed: {packed_count}\n"
|
|
217
|
+
"================================================================================\n"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
tree_section = (
|
|
221
|
+
"================================================================================\n"
|
|
222
|
+
"DIRECTORY TREE\n"
|
|
223
|
+
"================================================================================\n"
|
|
224
|
+
f"{tree_text}\n"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
files_section_header = (
|
|
228
|
+
"================================================================================\n"
|
|
229
|
+
"FILES CONTENT\n"
|
|
230
|
+
"================================================================================\n"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
files_content = "\n\n".join(content_blocks)
|
|
234
|
+
full_output = f"{header}\n{tree_section}\n{files_section_header}\n{files_content}\n"
|
|
235
|
+
|
|
236
|
+
# 5. Write to the output file
|
|
237
|
+
try:
|
|
238
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
240
|
+
f.write(full_output)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
raise IOError(f"Failed to write results to '{output_path}': {e}")
|
|
243
|
+
|
|
244
|
+
# 6. Copy to the system clipboard if enabled
|
|
245
|
+
clipboard_copied = False
|
|
246
|
+
clipboard_error = None
|
|
247
|
+
|
|
248
|
+
if copy_to_clipboard:
|
|
249
|
+
try:
|
|
250
|
+
pyperclip.copy(full_output)
|
|
251
|
+
clipboard_copied = True
|
|
252
|
+
except Exception as e:
|
|
253
|
+
clipboard_error = str(e)
|
|
254
|
+
print(f"Warning: Could not copy to clipboard: {e}", file=sys.stderr)
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
"root_dir": str(root_path),
|
|
258
|
+
"output_file": str(output_path),
|
|
259
|
+
"packed_count": packed_count,
|
|
260
|
+
"failed_count": failed_count,
|
|
261
|
+
"clipboard_copied": clipboard_copied,
|
|
262
|
+
"clipboard_error": clipboard_error,
|
|
263
|
+
"file_size": len(full_output)
|
|
264
|
+
}
|