ai-project-builder 2.0.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.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-project-builder
3
+ Version: 2.0.0
4
+ Summary: Fast Python project creation with AI
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fpdf2>=2.7.0
8
+
9
+ # AI Project Builder
10
+
11
+ Create Python projects instantly using AI.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pipx install . --system-site-packages
@@ -0,0 +1,8 @@
1
+ # AI Project Builder
2
+
3
+ Create Python projects instantly using AI.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pipx install . --system-site-packages
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ai-project-builder"
7
+ version = "2.0.0"
8
+ description = "Fast Python project creation with AI"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "fpdf2>=2.7.0",
13
+ ]
14
+
15
+ [project.scripts]
16
+ apb = "ai_project_builder.__main__:main"
17
+
18
+ [tool.setuptools.packages.find]
19
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,8 @@
1
+ def main():
2
+ from ai_project_builder.gui.app import App
3
+ app = App()
4
+ app.run()
5
+
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -0,0 +1,5 @@
1
+ from .escapes import escape_content, unescape_content
2
+ from .encoder import encode_project, encode_project_from_directory
3
+ from .decoder import decode_project, DecodeError
4
+ from .project_io import deploy_project, read_project, ProjectIOResult
5
+ from .report import generate_report
@@ -0,0 +1,110 @@
1
+ """
2
+ Декодирование APB-формата в структуру проекта.
3
+ """
4
+
5
+ import re
6
+ from .escapes import unescape_content
7
+
8
+
9
+ class DecodeError(Exception):
10
+ pass
11
+
12
+
13
+ def decode_project(text: str) -> dict:
14
+ """
15
+ Декодирует APB-формат в словарь проекта.
16
+
17
+ Формат:
18
+ ===APB_START===
19
+ PROJECT: name
20
+ MODE: create
21
+
22
+ ---FILE: path/to/file.py---
23
+ content here
24
+ ---END_FILE---
25
+
26
+ ===APB_END===
27
+ """
28
+ text = text.strip()
29
+
30
+ # Ищем границы
31
+ start_match = re.search(r'={3,}APB_START={3,}', text)
32
+ end_match = re.search(r'={3,}APB_END={3,}', text)
33
+
34
+ if not start_match:
35
+ raise DecodeError(
36
+ "Не найден маркер начала ===APB_START===\n"
37
+ "Убедитесь, что ИИ выдала правильный формат."
38
+ )
39
+
40
+ if not end_match:
41
+ raise DecodeError(
42
+ "Не найден маркер конца ===APB_END===\n"
43
+ "Возможно, ответ ИИ был обрезан."
44
+ )
45
+
46
+ # Извлекаем содержимое между маркерами
47
+ content = text[start_match.end():end_match.start()].strip()
48
+
49
+ # Парсим заголовок
50
+ project_match = re.search(r'PROJECT:\s*(.+)', content)
51
+ mode_match = re.search(r'MODE:\s*(\w+)', content)
52
+
53
+ if not project_match:
54
+ raise DecodeError("Не найдено поле PROJECT:")
55
+
56
+ project_name = project_match.group(1).strip()
57
+ mode = mode_match.group(1).strip().lower() if mode_match else "create"
58
+
59
+ # Парсим файлы
60
+ files = []
61
+
62
+ # Паттерн для поиска файлов: ---FILE: path--- ... ---END_FILE---
63
+ file_pattern = re.compile(
64
+ r'-{3,}FILE:\s*(.+?)\s*-{3,}\n(.*?)\n-{3,}END_FILE-{3,}',
65
+ re.DOTALL
66
+ )
67
+
68
+ for match in file_pattern.finditer(content):
69
+ file_path = match.group(1).strip()
70
+ file_content = match.group(2)
71
+
72
+ # Убираем один leading/trailing newline если есть
73
+ if file_content.startswith('\n'):
74
+ file_content = file_content[1:]
75
+ if file_content.endswith('\n'):
76
+ file_content = file_content[:-1]
77
+
78
+ # Применяем unescape
79
+ file_content = unescape_content(file_content)
80
+ file_path = unescape_content(file_path)
81
+
82
+ files.append({
83
+ "path": file_path,
84
+ "action": "write",
85
+ "content": file_content
86
+ })
87
+
88
+ # Ищем файлы для удаления
89
+ delete_pattern = re.compile(r'-{3,}DELETE:\s*(.+?)\s*-{3,}')
90
+ for match in delete_pattern.finditer(content):
91
+ file_path = unescape_content(match.group(1).strip())
92
+ files.append({
93
+ "path": file_path,
94
+ "action": "delete"
95
+ })
96
+
97
+ if not files:
98
+ raise DecodeError(
99
+ "Не найдено ни одного файла.\n"
100
+ "Файлы должны быть в формате:\n"
101
+ "---FILE: path/to/file.py---\n"
102
+ "content\n"
103
+ "---END_FILE---"
104
+ )
105
+
106
+ return {
107
+ "project": project_name,
108
+ "mode": mode,
109
+ "files": files
110
+ }
@@ -0,0 +1,80 @@
1
+ """
2
+ Кодирование проекта в APB-формат.
3
+ """
4
+
5
+ import os
6
+ from .escapes import escape_content
7
+
8
+
9
+ def encode_project(project_name: str, files: list, mode: str = "create") -> str:
10
+ """
11
+ Кодирует проект в APB-формат.
12
+ """
13
+ lines = [
14
+ "===APB_START===",
15
+ f"PROJECT: {project_name}",
16
+ f"MODE: {mode}",
17
+ ""
18
+ ]
19
+
20
+ for f in files:
21
+ path = f["path"]
22
+ action = f.get("action", "write")
23
+
24
+ if action == "delete":
25
+ lines.append(f"---DELETE: {path}---")
26
+ lines.append("")
27
+ else:
28
+ content = f.get("content", "")
29
+ escaped_content = escape_content(content)
30
+
31
+ lines.append(f"---FILE: {path}---")
32
+ lines.append(escaped_content)
33
+ lines.append("---END_FILE---")
34
+ lines.append("")
35
+
36
+ lines.append("===APB_END===")
37
+
38
+ return "\n".join(lines)
39
+
40
+
41
+ def encode_project_from_directory(project_dir: str, mode: str = "update") -> str:
42
+ """
43
+ Читает проект с диска и кодирует в APB-формат.
44
+ """
45
+ project_name = os.path.basename(os.path.normpath(project_dir))
46
+ files = []
47
+
48
+ skip_dirs = {".git", "__pycache__", "node_modules", ".venv", "venv", ".idea", ".vscode", "dist", "build", "*.egg-info"}
49
+ skip_extensions = {".pyc", ".pyo", ".so", ".dll", ".exe", ".bin"}
50
+
51
+ for root, dirs, filenames in os.walk(project_dir):
52
+ # Фильтруем директории
53
+ dirs[:] = [d for d in dirs if d not in skip_dirs and not d.endswith(".egg-info")]
54
+
55
+ for filename in sorted(filenames):
56
+ # Пропускаем скрытые и бинарные файлы
57
+ if filename.startswith("."):
58
+ continue
59
+
60
+ ext = os.path.splitext(filename)[1].lower()
61
+ if ext in skip_extensions:
62
+ continue
63
+
64
+ filepath = os.path.join(root, filename)
65
+ relpath = os.path.relpath(filepath, project_dir).replace(os.sep, "/")
66
+
67
+ try:
68
+ with open(filepath, "r", encoding="utf-8") as f:
69
+ content = f.read()
70
+
71
+ files.append({
72
+ "path": relpath,
73
+ "action": "write",
74
+ "content": content
75
+ })
76
+ except (UnicodeDecodeError, PermissionError, OSError):
77
+ # Пропускаем бинарные/недоступные файлы
78
+ continue
79
+
80
+ return encode_project(project_name, files, mode)
@@ -0,0 +1,69 @@
1
+ """
2
+ Escape-последовательности для безопасной передачи кода через ИИ.
3
+ Заменяем символы, которые Markdown может испортить.
4
+ """
5
+
6
+ # Порядок важен! Сначала длинные последовательности, потом короткие.
7
+ ESCAPE_MAP = [
8
+ # Тройные последовательности (сначала!)
9
+ ("```", "{{TICK3}}"),
10
+ ("---", "{{DASH3}}"),
11
+ ("===", "{{EQUAL3}}"),
12
+ ("***", "{{STAR3}}"),
13
+ ("___", "{{UNDER3}}"),
14
+
15
+ # Двойные
16
+ ("**", "{{STAR2}}"),
17
+ ("__", "{{UNDER2}}"),
18
+ ("~~", "{{TILDE2}}"),
19
+
20
+ # Одиночные (в конце)
21
+ ("`", "{{TICK}}"),
22
+ ("#", "{{HASH}}"),
23
+ ("*", "{{STAR}}"),
24
+ ("_", "{{UNDER}}"),
25
+ (">", "{{GT}}"),
26
+ ("<", "{{LT}}"),
27
+ ("[", "{{LBRACKET}}"),
28
+ ("]", "{{RBRACKET}}"),
29
+ ("(", "{{LPAREN}}"),
30
+ (")", "{{RPAREN}}"),
31
+ ("|", "{{PIPE}}"),
32
+ ("\\", "{{BACKSLASH}}"),
33
+ ]
34
+
35
+ # Обратная карта для декодирования
36
+ UNESCAPE_MAP = [(escaped, original) for original, escaped in ESCAPE_MAP]
37
+
38
+
39
+ def escape_content(text: str) -> str:
40
+ """
41
+ Заменяет опасные символы на escape-последовательности.
42
+ Используется при кодировании проекта для отправки в ИИ.
43
+ """
44
+ result = text
45
+ for original, escaped in ESCAPE_MAP:
46
+ result = result.replace(original, escaped)
47
+ return result
48
+
49
+
50
+ def unescape_content(text: str) -> str:
51
+ """
52
+ Заменяет escape-последов��тельности обратно на символы.
53
+ Используется при декодировании ответа от ИИ.
54
+ """
55
+ result = text
56
+ for escaped, original in UNESCAPE_MAP:
57
+ result = result.replace(escaped, original)
58
+ return result
59
+
60
+
61
+ def get_escape_reference() -> str:
62
+ """
63
+ Возвращает справку по escape-последовательностям для промпта.
64
+ """
65
+ lines = ["ESCAPE SEQUENCES (use these instead of actual characters):"]
66
+ for original, escaped in ESCAPE_MAP:
67
+ display_original = repr(original)[1:-1] # Убираем кавычки
68
+ lines.append(f" {escaped} = {display_original}")
69
+ return "\n".join(lines)
@@ -0,0 +1,144 @@
1
+ """
2
+ Создание и обновление проектов на диске.
3
+ """
4
+
5
+ import os
6
+
7
+
8
+ class ProjectIOResult:
9
+ def __init__(self):
10
+ self.created_files: list[str] = []
11
+ self.updated_files: list[str] = []
12
+ self.deleted_files: list[str] = []
13
+ self.created_dirs: list[str] = []
14
+ self.errors: list[str] = []
15
+
16
+ def summary(self) -> str:
17
+ lines = []
18
+
19
+ if self.created_dirs:
20
+ lines.append(f"📁 Создано папок: {len(self.created_dirs)}")
21
+
22
+ if self.created_files:
23
+ lines.append(f"✅ Создано файлов: {len(self.created_files)}")
24
+ for f in self.created_files[:10]:
25
+ lines.append(f" + {f}")
26
+ if len(self.created_files) > 10:
27
+ lines.append(f" ... и ещё {len(self.created_files) - 10}")
28
+
29
+ if self.updated_files:
30
+ lines.append(f"📝 Обновлено файлов: {len(self.updated_files)}")
31
+ for f in self.updated_files[:10]:
32
+ lines.append(f" ~ {f}")
33
+ if len(self.updated_files) > 10:
34
+ lines.append(f" ... и ещё {len(self.updated_files) - 10}")
35
+
36
+ if self.deleted_files:
37
+ lines.append(f"🗑️ Удалено файлов: {len(self.deleted_files)}")
38
+ for f in self.deleted_files:
39
+ lines.append(f" - {f}")
40
+
41
+ if self.errors:
42
+ lines.append(f"❌ Ошибки: {len(self.errors)}")
43
+ for e in self.errors:
44
+ lines.append(f" ! {e}")
45
+
46
+ return "\n".join(lines) if lines else "Нет изменений."
47
+
48
+
49
+ def deploy_project(package: dict, base_dir: str) -> ProjectIOResult:
50
+ """
51
+ Разворачивает проект из декодированного пакета.
52
+ """
53
+ result = ProjectIOResult()
54
+ project_name = package["project"]
55
+ project_dir = os.path.join(base_dir, project_name)
56
+
57
+ # Создаём корневую папку
58
+ if not os.path.exists(project_dir):
59
+ os.makedirs(project_dir)
60
+ result.created_dirs.append(project_name)
61
+
62
+ for file_info in package["files"]:
63
+ path = file_info["path"]
64
+ action = file_info.get("action", "write")
65
+
66
+ # Защита от path traversal
67
+ full_path = os.path.normpath(os.path.join(project_dir, path))
68
+ if not full_path.startswith(os.path.normpath(project_dir)):
69
+ result.errors.append(f"Опасный путь: {path}")
70
+ continue
71
+
72
+ if action == "delete":
73
+ if os.path.exists(full_path):
74
+ try:
75
+ os.remove(full_path)
76
+ result.deleted_files.append(path)
77
+ except OSError as e:
78
+ result.errors.append(f"Не удалось удалить {path}: {e}")
79
+
80
+ elif action == "write":
81
+ content = file_info.get("content", "")
82
+
83
+ # Создаём поддиректории
84
+ file_dir = os.path.dirname(full_path)
85
+ if file_dir and not os.path.exists(file_dir):
86
+ os.makedirs(file_dir, exist_ok=True)
87
+ rel_dir = os.path.relpath(file_dir, project_dir)
88
+ if rel_dir not in result.created_dirs:
89
+ result.created_dirs.append(rel_dir)
90
+
91
+ is_update = os.path.exists(full_path)
92
+
93
+ try:
94
+ with open(full_path, "w", encoding="utf-8", newline="\n") as f:
95
+ f.write(content)
96
+
97
+ if is_update:
98
+ result.updated_files.append(path)
99
+ else:
100
+ result.created_files.append(path)
101
+ except OSError as e:
102
+ result.errors.append(f"Ошибка записи {path}: {e}")
103
+
104
+ return result
105
+
106
+
107
+ def read_project(project_dir: str) -> dict | None:
108
+ """
109
+ Читает существующий проект с диска.
110
+ """
111
+ if not os.path.isdir(project_dir):
112
+ return None
113
+
114
+ project_name = os.path.basename(os.path.normpath(project_dir))
115
+ files = []
116
+
117
+ skip_dirs = {".git", "__pycache__", "node_modules", ".venv", "venv", ".idea", ".vscode"}
118
+
119
+ for root, dirs, filenames in os.walk(project_dir):
120
+ dirs[:] = [d for d in dirs if d not in skip_dirs and not d.startswith(".")]
121
+
122
+ for filename in sorted(filenames):
123
+ if filename.startswith("."):
124
+ continue
125
+
126
+ filepath = os.path.join(root, filename)
127
+ relpath = os.path.relpath(filepath, project_dir).replace(os.sep, "/")
128
+
129
+ try:
130
+ with open(filepath, "r", encoding="utf-8") as f:
131
+ content = f.read()
132
+ files.append({
133
+ "path": relpath,
134
+ "action": "write",
135
+ "content": content
136
+ })
137
+ except (UnicodeDecodeError, PermissionError):
138
+ continue
139
+
140
+ return {
141
+ "project": project_name,
142
+ "mode": "update",
143
+ "files": files
144
+ }
@@ -0,0 +1,172 @@
1
+ """
2
+ Генерация компактного PDF-отчёта по проекту.
3
+ """
4
+
5
+ import os
6
+ from fpdf import FPDF
7
+
8
+
9
+ class ProjectReport(FPDF):
10
+ def __init__(self):
11
+ super().__init__()
12
+ self._font_ready = False
13
+ self._has_unicode = False
14
+ # Компактные поля страницы
15
+ self.set_margins(10, 10, 10)
16
+ self.set_auto_page_break(auto=True, margin=10)
17
+
18
+ def _setup_fonts(self):
19
+ if self._font_ready:
20
+ return
21
+
22
+ font_paths = [
23
+ "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
24
+ "/usr/share/fonts/TTF/DejaVuSansMono.ttf",
25
+ "C:/Windows/Fonts/consola.ttf",
26
+ "C:/Windows/Fonts/lucon.ttf",
27
+ ]
28
+
29
+ for fp in font_paths:
30
+ if os.path.exists(fp):
31
+ try:
32
+ self.add_font("Mono", "", fp, uni=True)
33
+ self._has_unicode = True
34
+ break
35
+ except:
36
+ pass
37
+
38
+ self._font_ready = True
39
+
40
+ def set_mono(self, size=7):
41
+ self._setup_fonts()
42
+ if self._has_unicode:
43
+ self.set_font("Mono", "", size)
44
+ else:
45
+ self.set_font("Courier", "", size)
46
+
47
+ def header(self):
48
+ # Минимальный заголовок
49
+ self.set_mono(6)
50
+ self.set_text_color(128, 128, 128)
51
+ self.cell(0, 3, "AI Project Builder Report", 0, 1, "R")
52
+ self.set_text_color(0, 0, 0)
53
+
54
+ def footer(self):
55
+ self.set_y(-8)
56
+ self.set_mono(6)
57
+ self.set_text_color(128, 128, 128)
58
+ self.cell(0, 3, f"{self.page_no()}", 0, 0, "C")
59
+ self.set_text_color(0, 0, 0)
60
+
61
+
62
+ def generate_report(package: dict, output_path: str) -> str:
63
+ """
64
+ Генерирует компактный PDF-отчёт по проекту.
65
+ Все файлы идут подряд без разрывов страниц.
66
+ """
67
+ pdf = ProjectReport()
68
+ pdf.add_page()
69
+
70
+ # === Заголовок проекта (компактный) ===
71
+ pdf.set_mono(10)
72
+ pdf.cell(0, 5, f"PROJECT: {package['project']}", 0, 1)
73
+ pdf.set_mono(7)
74
+ pdf.cell(0, 3, f"Mode: {package.get('mode', 'create')} | Files: {len(package['files'])}", 0, 1)
75
+
76
+ # === Структура файлов (компактный список) ===
77
+ pdf.ln(2)
78
+ pdf.set_mono(6)
79
+ pdf.set_text_color(80, 80, 80)
80
+
81
+ structure_lines = []
82
+ for f in package["files"]:
83
+ icon = "+" if f.get("action") == "write" else "-"
84
+ lines = f.get("content", "").count("\n") + 1 if f.get("content") else 0
85
+ structure_lines.append(f"[{icon}] {f['path']} ({lines}L)")
86
+
87
+ # Выводим структуру в 2-3 колонки если много файлов
88
+ struct_text = " | ".join(structure_lines)
89
+ pdf.multi_cell(0, 3, struct_text)
90
+
91
+ pdf.set_text_color(0, 0, 0)
92
+ pdf.ln(3)
93
+
94
+ # Разделитель
95
+ pdf.set_draw_color(200, 200, 200)
96
+ pdf.line(10, pdf.get_y(), 200, pdf.get_y())
97
+ pdf.ln(2)
98
+
99
+ # === Содержимое файлов (подряд, без разрывов) ===
100
+ for f in package["files"]:
101
+ if f.get("action") == "delete":
102
+ continue
103
+
104
+ content = f.get("content", "")
105
+ path = f["path"]
106
+
107
+ # Заголовок файла
108
+ pdf.set_fill_color(240, 240, 240)
109
+ pdf.set_mono(7)
110
+ pdf.cell(0, 4, f"=== {path} ===", 0, 1, "L", True)
111
+
112
+ if not content.strip():
113
+ pdf.set_mono(6)
114
+ pdf.set_text_color(128, 128, 128)
115
+ pdf.cell(0, 3, "(empty)", 0, 1)
116
+ pdf.set_text_color(0, 0, 0)
117
+ else:
118
+ pdf.set_mono(5) # Мелкий шрифт для кода
119
+
120
+ lines = content.split("\n")
121
+ line_height = 2.5 # Очень компактная высота строки
122
+
123
+ for i, line in enumerate(lines, 1):
124
+ # Обрезаем очень длинные строки
125
+ display = line.replace("\t", " ") # Табы в 2 пробела
126
+ if len(display) > 120:
127
+ display = display[:117] + "..."
128
+
129
+ # Номер строки + код
130
+ line_text = f"{i:3d}|{display}"
131
+
132
+ try:
133
+ pdf.cell(0, line_height, line_text, 0, 1)
134
+ except:
135
+ # Если символ не поддерживается
136
+ safe = line_text.encode("ascii", errors="replace").decode()
137
+ pdf.cell(0, line_height, safe, 0, 1)
138
+
139
+ # Минимальный отступ между файлами
140
+ pdf.ln(1)
141
+
142
+ pdf.output(output_path)
143
+ return output_path
144
+
145
+
146
+ def generate_text_report(package: dict) -> str:
147
+ """
148
+ Генерирует компактный текстовый отчёт.
149
+ """
150
+ lines = [
151
+ f"PROJECT: {package['project']}",
152
+ f"MODE: {package.get('mode', 'create')} | FILES: {len(package['files'])}",
153
+ "=" * 70,
154
+ ]
155
+
156
+ # Компактная структура
157
+ for f in package["files"]:
158
+ icon = "+" if f.get("action") == "write" else "-"
159
+ lc = f.get("content", "").count("\n") + 1 if f.get("content") else 0
160
+ lines.append(f"[{icon}] {f['path']} ({lc}L)")
161
+
162
+ lines.append("=" * 70)
163
+
164
+ # Файлы подряд
165
+ for f in package["files"]:
166
+ if f.get("action") == "delete":
167
+ continue
168
+
169
+ lines.append(f"\n### {f['path']} ###")
170
+ lines.append(f.get("content", "") or "(empty)")
171
+
172
+ return "\n".join(lines)
@@ -0,0 +1,520 @@
1
+ """
2
+ GUI приложение на Tkinter.
3
+ """
4
+
5
+ import os
6
+ import tkinter as tk
7
+ from tkinter import ttk, filedialog, messagebox, scrolledtext
8
+
9
+ from ai_project_builder.core import (
10
+ encode_project_from_directory,
11
+ decode_project,
12
+ DecodeError,
13
+ deploy_project,
14
+ read_project,
15
+ generate_report,
16
+ )
17
+ from ai_project_builder.core.escapes import get_escape_reference
18
+ from ai_project_builder.core.report import generate_text_report
19
+
20
+
21
+ PROMPT_TEMPLATE = '''You are a code generator. Create a Python project based on my description.
22
+
23
+ OUTPUT FORMAT - Follow EXACTLY:
24
+
25
+ ===APB_START===
26
+ PROJECT: project_name_here
27
+ MODE: create
28
+
29
+ ---FILE: path/to/file.py---
30
+ file content here
31
+ ---END_FILE---
32
+
33
+ ---FILE: another/file.py---
34
+ another file content
35
+ ---END_FILE---
36
+
37
+ ===APB_END===
38
+
39
+ {escape_reference}
40
+
41
+ EXAMPLE of correct Python code in this format:
42
+
43
+ ---FILE: main.py---
44
+ def hello():
45
+ print("Hello, World!")
46
+
47
+ if {{UNDER}}{{UNDER}}name{{UNDER}}{{UNDER}} == "{{UNDER}}{{UNDER}}main{{UNDER}}{{UNDER}}":
48
+ hello()
49
+ ---END_FILE---
50
+
51
+ Notice: parentheses () and brackets [] are written directly, NOT escaped!
52
+
53
+ CRITICAL RULES:
54
+ 1. Start with ===APB_START=== end with ===APB_END===
55
+ 2. Each file: ---FILE: path--- content ---END_FILE---
56
+ 3. Escape ONLY: * _ ` # > < | (see list above)
57
+ 4. Do NOT escape: ( ) [ ] {{ }} = : ; , . / \\ " '
58
+ 5. Include ALL files: main.py, __init__.py, requirements.txt, README.md
59
+ 6. Code must be complete and runnable
60
+
61
+ PROJECT DESCRIPTION:
62
+ {description}
63
+
64
+ Generate the complete project now:'''
65
+
66
+
67
+ UPDATE_PROMPT_TEMPLATE = '''You are a code editor. Modify the existing project based on my request.
68
+
69
+ CURRENT PROJECT:
70
+ {current_project}
71
+
72
+ REQUESTED CHANGES:
73
+ {changes}
74
+
75
+ OUTPUT FORMAT - Include ONLY changed files:
76
+
77
+ ===APB_START===
78
+ PROJECT: project_name_here
79
+ MODE: update
80
+
81
+ ---FILE: path/to/changed_file.py---
82
+ complete new content of file
83
+ ---END_FILE---
84
+
85
+ ---DELETE: path/to/removed_file.py---
86
+
87
+ ===APB_END===
88
+
89
+ {escape_reference}
90
+
91
+ RULES:
92
+ 1. Only include files that need to be changed or deleted
93
+ 2. For changed files, include the COMPLETE new content
94
+ 3. Use ---DELETE: path--- for files to remove
95
+ 4. Use escape sequences for special characters
96
+
97
+ Generate the changes now:'''
98
+
99
+
100
+ class App:
101
+ def __init__(self):
102
+ self.root = tk.Tk()
103
+ self.root.title("AI Project Builder v2")
104
+ self.root.geometry("950x750")
105
+ self.root.minsize(800, 600)
106
+
107
+ self.output_dir = tk.StringVar(value=os.path.expanduser("~/Projects"))
108
+ self.project_dir = tk.StringVar()
109
+ self.encode_dir = tk.StringVar()
110
+
111
+ self._build_ui()
112
+
113
+ def _build_ui(self):
114
+ notebook = ttk.Notebook(self.root)
115
+ notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
116
+
117
+ self._build_prompt_tab(notebook)
118
+ self._build_deploy_tab(notebook)
119
+ self._build_report_tab(notebook)
120
+ self._build_encode_tab(notebook)
121
+ self._build_help_tab(notebook)
122
+
123
+ self.status = tk.StringVar(value="Ready")
124
+ ttk.Label(
125
+ self.root,
126
+ textvariable=self.status,
127
+ relief=tk.SUNKEN,
128
+ anchor=tk.W,
129
+ padding=(10, 5)
130
+ ).pack(fill=tk.X, padx=10, pady=(0, 10))
131
+
132
+ # ==================== PROMPT TAB ====================
133
+ def _build_prompt_tab(self, notebook):
134
+ tab = ttk.Frame(notebook, padding=10)
135
+ notebook.add(tab, text=" 📝 Generate Prompt ")
136
+
137
+ ttk.Label(tab, text="Describe your project:", font=("", 11, "bold")).pack(anchor=tk.W)
138
+
139
+ self.description = scrolledtext.ScrolledText(tab, height=5, font=("Consolas", 10), wrap=tk.WORD)
140
+ self.description.pack(fill=tk.X, pady=(5, 10))
141
+ self.description.insert("1.0", "Example: A CLI tool that converts CSV files to JSON with filtering support")
142
+
143
+ btn_frame = ttk.Frame(tab)
144
+ btn_frame.pack(fill=tk.X, pady=5)
145
+
146
+ ttk.Button(btn_frame, text="🆕 Create Project Prompt", command=self._gen_create_prompt).pack(side=tk.LEFT, padx=(0, 5))
147
+ ttk.Button(btn_frame, text="✏️ Update Project Prompt", command=self._gen_update_prompt).pack(side=tk.LEFT, padx=(0, 5))
148
+ ttk.Button(btn_frame, text="📋 Copy to Clipboard", command=self._copy_prompt).pack(side=tk.RIGHT)
149
+
150
+ ttk.Label(tab, text="Generated prompt (copy to AI):", font=("", 11, "bold")).pack(anchor=tk.W, pady=(15, 5))
151
+
152
+ self.prompt_output = scrolledtext.ScrolledText(tab, font=("Consolas", 9), wrap=tk.WORD)
153
+ self.prompt_output.pack(fill=tk.BOTH, expand=True)
154
+
155
+ def _gen_create_prompt(self):
156
+ desc = self.description.get("1.0", tk.END).strip()
157
+ if not desc:
158
+ messagebox.showwarning("Warning", "Enter project description")
159
+ return
160
+
161
+ prompt = PROMPT_TEMPLATE.format(
162
+ description=desc,
163
+ escape_reference=get_escape_reference()
164
+ )
165
+
166
+ self.prompt_output.delete("1.0", tk.END)
167
+ self.prompt_output.insert("1.0", prompt)
168
+ self.status.set("Create prompt generated - copy and paste to AI")
169
+
170
+ def _gen_update_prompt(self):
171
+ desc = self.description.get("1.0", tk.END).strip()
172
+ if not desc:
173
+ messagebox.showwarning("Warning", "Enter change description")
174
+ return
175
+
176
+ path = filedialog.askdirectory(title="Select existing project folder")
177
+ if not path:
178
+ return
179
+
180
+ try:
181
+ encoded = encode_project_from_directory(path, mode="update")
182
+ except Exception as e:
183
+ messagebox.showerror("Error", f"Failed to read project:\n{e}")
184
+ return
185
+
186
+ prompt = UPDATE_PROMPT_TEMPLATE.format(
187
+ current_project=encoded,
188
+ changes=desc,
189
+ escape_reference=get_escape_reference()
190
+ )
191
+
192
+ self.prompt_output.delete("1.0", tk.END)
193
+ self.prompt_output.insert("1.0", prompt)
194
+ self.status.set(f"Update prompt generated for: {os.path.basename(path)}")
195
+
196
+ def _copy_prompt(self):
197
+ content = self.prompt_output.get("1.0", tk.END).strip()
198
+ if not content:
199
+ messagebox.showwarning("Warning", "No prompt to copy")
200
+ return
201
+
202
+ self.root.clipboard_clear()
203
+ self.root.clipboard_append(content)
204
+ self.status.set("Prompt copied to clipboard!")
205
+
206
+ # ==================== DEPLOY TAB ====================
207
+ def _build_deploy_tab(self, notebook):
208
+ tab = ttk.Frame(notebook, padding=10)
209
+ notebook.add(tab, text=" 🚀 Deploy Project ")
210
+
211
+ dir_frame = ttk.LabelFrame(tab, text="Output Directory", padding=10)
212
+ dir_frame.pack(fill=tk.X, pady=(0, 10))
213
+
214
+ dir_row = ttk.Frame(dir_frame)
215
+ dir_row.pack(fill=tk.X)
216
+ ttk.Entry(dir_row, textvariable=self.output_dir, font=("Consolas", 10)).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
217
+ ttk.Button(dir_row, text="Browse...", command=self._browse_output).pack(side=tk.RIGHT)
218
+
219
+ ttk.Label(tab, text="Paste AI response here:", font=("", 11, "bold")).pack(anchor=tk.W)
220
+
221
+ self.ai_response = scrolledtext.ScrolledText(tab, height=15, font=("Consolas", 9))
222
+ self.ai_response.pack(fill=tk.BOTH, expand=True, pady=(5, 10))
223
+
224
+ btn_frame = ttk.Frame(tab)
225
+ btn_frame.pack(fill=tk.X, pady=5)
226
+
227
+ ttk.Button(btn_frame, text="🔍 Validate", command=self._validate).pack(side=tk.LEFT, padx=(0, 5))
228
+ ttk.Button(btn_frame, text="🚀 Deploy Project", command=self._deploy).pack(side=tk.LEFT, padx=(0, 5))
229
+ ttk.Button(btn_frame, text="🗑️ Clear", command=lambda: self.ai_response.delete("1.0", tk.END)).pack(side=tk.RIGHT)
230
+
231
+ ttk.Label(tab, text="Result:", font=("", 10)).pack(anchor=tk.W, pady=(10, 0))
232
+ self.deploy_log = scrolledtext.ScrolledText(tab, height=6, font=("Consolas", 9), state=tk.DISABLED)
233
+ self.deploy_log.pack(fill=tk.X)
234
+
235
+ def _browse_output(self):
236
+ path = filedialog.askdirectory(title="Select output directory")
237
+ if path:
238
+ self.output_dir.set(path)
239
+
240
+ def _validate(self):
241
+ text = self.ai_response.get("1.0", tk.END).strip()
242
+ if not text:
243
+ messagebox.showwarning("Warning", "Paste AI response first")
244
+ return
245
+
246
+ try:
247
+ pkg = decode_project(text)
248
+
249
+ files_info = []
250
+ for f in pkg["files"]:
251
+ icon = "📄" if f.get("action") == "write" else "🗑️"
252
+ lines = f.get("content", "").count("\n") + 1 if f.get("content") else 0
253
+ files_info.append(f" {icon} {f['path']} ({lines} lines)")
254
+
255
+ self._log_deploy(
256
+ f"✅ Validation successful!\n\n"
257
+ f"Project: {pkg['project']}\n"
258
+ f"Mode: {pkg['mode']}\n"
259
+ f"Files: {len(pkg['files'])}\n\n"
260
+ + "\n".join(files_info)
261
+ )
262
+ self.status.set("Validation passed")
263
+
264
+ except DecodeError as e:
265
+ self._log_deploy(f"❌ Validation failed:\n\n{e}")
266
+ self.status.set("Validation failed")
267
+
268
+ def _deploy(self):
269
+ text = self.ai_response.get("1.0", tk.END).strip()
270
+ output = self.output_dir.get().strip()
271
+
272
+ if not text:
273
+ messagebox.showwarning("Warning", "Paste AI response first")
274
+ return
275
+ if not output:
276
+ messagebox.showwarning("Warning", "Select output directory")
277
+ return
278
+
279
+ try:
280
+ pkg = decode_project(text)
281
+ except DecodeError as e:
282
+ messagebox.showerror("Decode Error", str(e))
283
+ return
284
+
285
+ project_path = os.path.join(output, pkg["project"])
286
+
287
+ if os.path.exists(project_path):
288
+ if not messagebox.askyesno(
289
+ "Project Exists",
290
+ f"'{pkg['project']}' already exists.\n\nUpdate existing files?"
291
+ ):
292
+ return
293
+
294
+ os.makedirs(output, exist_ok=True)
295
+
296
+ try:
297
+ result = deploy_project(pkg, output)
298
+ self._log_deploy(
299
+ f"✅ Project deployed!\n\n"
300
+ f"Location: {project_path}\n\n"
301
+ f"{result.summary()}"
302
+ )
303
+ self.status.set(f"Deployed: {project_path}")
304
+ except Exception as e:
305
+ messagebox.showerror("Deploy Error", str(e))
306
+
307
+ def _log_deploy(self, text: str):
308
+ self.deploy_log.config(state=tk.NORMAL)
309
+ self.deploy_log.delete("1.0", tk.END)
310
+ self.deploy_log.insert("1.0", text)
311
+ self.deploy_log.config(state=tk.DISABLED)
312
+
313
+ # ==================== REPORT TAB ====================
314
+ def _build_report_tab(self, notebook):
315
+ tab = ttk.Frame(notebook, padding=10)
316
+ notebook.add(tab, text=" 📄 Report ")
317
+
318
+ ttk.Label(tab, text="Generate PDF report from existing project", font=("", 12, "bold")).pack(anchor=tk.W)
319
+
320
+ dir_frame = ttk.Frame(tab)
321
+ dir_frame.pack(fill=tk.X, pady=15)
322
+
323
+ ttk.Label(dir_frame, text="Project folder:").pack(anchor=tk.W)
324
+ row = ttk.Frame(dir_frame)
325
+ row.pack(fill=tk.X, pady=5)
326
+ ttk.Entry(row, textvariable=self.project_dir, font=("Consolas", 10)).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
327
+ ttk.Button(row, text="Browse...", command=lambda: self._browse_var(self.project_dir)).pack(side=tk.RIGHT)
328
+
329
+ btn_frame = ttk.Frame(tab)
330
+ btn_frame.pack(fill=tk.X, pady=10)
331
+
332
+ ttk.Button(btn_frame, text="💾 Save PDF Report", command=self._save_pdf).pack(side=tk.LEFT, padx=(0, 5))
333
+ ttk.Button(btn_frame, text="📋 Copy Text Report", command=self._copy_text_report).pack(side=tk.LEFT)
334
+
335
+ ttk.Label(tab, text="Preview:", font=("", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
336
+ self.report_preview = scrolledtext.ScrolledText(tab, font=("Consolas", 9), state=tk.DISABLED)
337
+ self.report_preview.pack(fill=tk.BOTH, expand=True)
338
+
339
+ def _save_pdf(self):
340
+ path = self.project_dir.get().strip()
341
+ if not path or not os.path.isdir(path):
342
+ messagebox.showwarning("Warning", "Select valid project folder")
343
+ return
344
+
345
+ pkg = read_project(path)
346
+ if not pkg or not pkg["files"]:
347
+ messagebox.showwarning("Warning", "Project is empty or unreadable")
348
+ return
349
+
350
+ output = filedialog.asksaveasfilename(
351
+ title="Save PDF Report",
352
+ defaultextension=".pdf",
353
+ filetypes=[("PDF", "*.pdf")],
354
+ initialfile=f"{pkg['project']}_report.pdf"
355
+ )
356
+ if not output:
357
+ return
358
+
359
+ try:
360
+ generate_report(pkg, output)
361
+ self._show_preview(pkg)
362
+ messagebox.showinfo("Success", f"Report saved:\n{output}")
363
+ self.status.set(f"Report saved: {output}")
364
+ except Exception as e:
365
+ messagebox.showerror("Error", str(e))
366
+
367
+ def _copy_text_report(self):
368
+ path = self.project_dir.get().strip()
369
+ if not path or not os.path.isdir(path):
370
+ messagebox.showwarning("Warning", "Select valid project folder")
371
+ return
372
+
373
+ pkg = read_project(path)
374
+ if not pkg:
375
+ return
376
+
377
+ text = generate_text_report(pkg)
378
+ self.root.clipboard_clear()
379
+ self.root.clipboard_append(text)
380
+ self._show_preview(pkg)
381
+ self.status.set("Text report copied to clipboard")
382
+
383
+ def _show_preview(self, pkg: dict):
384
+ text = generate_text_report(pkg)
385
+ self.report_preview.config(state=tk.NORMAL)
386
+ self.report_preview.delete("1.0", tk.END)
387
+ self.report_preview.insert("1.0", text)
388
+ self.report_preview.config(state=tk.DISABLED)
389
+
390
+ # ==================== ENCODE TAB ====================
391
+ def _build_encode_tab(self, notebook):
392
+ tab = ttk.Frame(notebook, padding=10)
393
+ notebook.add(tab, text=" 🔒 Encode Project ")
394
+
395
+ ttk.Label(tab, text="Encode existing project for AI editing", font=("", 12, "bold")).pack(anchor=tk.W)
396
+
397
+ dir_frame = ttk.Frame(tab)
398
+ dir_frame.pack(fill=tk.X, pady=15)
399
+
400
+ ttk.Label(dir_frame, text="Project folder:").pack(anchor=tk.W)
401
+ row = ttk.Frame(dir_frame)
402
+ row.pack(fill=tk.X, pady=5)
403
+ ttk.Entry(row, textvariable=self.encode_dir, font=("Consolas", 10)).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
404
+ ttk.Button(row, text="Browse...", command=lambda: self._browse_var(self.encode_dir)).pack(side=tk.RIGHT)
405
+
406
+ btn_frame = ttk.Frame(tab)
407
+ btn_frame.pack(fill=tk.X, pady=5)
408
+
409
+ ttk.Button(btn_frame, text="🔒 Encode", command=self._encode).pack(side=tk.LEFT, padx=(0, 5))
410
+ ttk.Button(btn_frame, text="📋 Copy", command=self._copy_encoded).pack(side=tk.LEFT)
411
+
412
+ ttk.Label(tab, text="Encoded project:", font=("", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
413
+ self.encoded_output = scrolledtext.ScrolledText(tab, font=("Consolas", 9))
414
+ self.encoded_output.pack(fill=tk.BOTH, expand=True)
415
+
416
+ def _encode(self):
417
+ path = self.encode_dir.get().strip()
418
+ if not path or not os.path.isdir(path):
419
+ messagebox.showwarning("Warning", "Select valid project folder")
420
+ return
421
+
422
+ try:
423
+ encoded = encode_project_from_directory(path)
424
+ self.encoded_output.delete("1.0", tk.END)
425
+ self.encoded_output.insert("1.0", encoded)
426
+
427
+ size_kb = len(encoded) / 1024
428
+ self.status.set(f"Encoded: {size_kb:.1f} KB")
429
+ except Exception as e:
430
+ messagebox.showerror("Error", str(e))
431
+
432
+ def _copy_encoded(self):
433
+ content = self.encoded_output.get("1.0", tk.END).strip()
434
+ if not content:
435
+ return
436
+
437
+ self.root.clipboard_clear()
438
+ self.root.clipboard_append(content)
439
+ self.status.set("Encoded project copied to clipboard")
440
+
441
+ # ==================== HELP TAB ====================
442
+ def _build_help_tab(self, notebook):
443
+ tab = ttk.Frame(notebook, padding=10)
444
+ notebook.add(tab, text=" ❓ Help ")
445
+
446
+ help_text = """
447
+ AI PROJECT BUILDER v2.0
448
+ =======================
449
+
450
+ HOW TO USE:
451
+
452
+ 1. GENERATE PROMPT
453
+ - Write a description of your project
454
+ - Click "Create Project Prompt"
455
+ - Copy the generated prompt
456
+
457
+ 2. ASK AI
458
+ - Paste the prompt into ChatGPT, Claude, or any AI
459
+ - The AI will generate code in APB format
460
+
461
+ 3. DEPLOY
462
+ - Copy the AI response
463
+ - Paste into "Deploy Project" tab
464
+ - Click "Validate" to check
465
+ - Click "Deploy Project" to create files
466
+
467
+ 4. UPDATE EXISTING PROJECT
468
+ - Use "Update Project Prompt" button
469
+ - Select existing project folder
470
+ - Describe changes you want
471
+
472
+ APB FORMAT:
473
+ ===========
474
+ ===APB_START===
475
+ PROJECT: my_project
476
+ MODE: create
477
+
478
+ ---FILE: main.py---
479
+ print("Hello!")
480
+ ---END_FILE---
481
+
482
+ ===APB_END===
483
+
484
+ ESCAPE SEQUENCES:
485
+ =================
486
+ The AI uses special sequences to avoid Markdown issues:
487
+
488
+ {{STAR}} → *
489
+ {{STAR2}} → **
490
+ {{HASH}} → #
491
+ {{TICK}} → `
492
+ {{TICK3}} → ```
493
+ {{GT}} → >
494
+ {{LT}} → <
495
+
496
+ Parentheses () [] {} are NOT escaped - they are safe!
497
+
498
+ These are automatically converted when deploying.
499
+
500
+ TIPS:
501
+ =====
502
+ • Use Claude or GPT-4 for best results
503
+ • For complex projects, generate in parts
504
+ • Always validate before deploying
505
+ • Check the "Encode" tab to share existing projects
506
+ """
507
+
508
+ text = scrolledtext.ScrolledText(tab, font=("Consolas", 10), wrap=tk.WORD)
509
+ text.pack(fill=tk.BOTH, expand=True)
510
+ text.insert("1.0", help_text.strip())
511
+ text.config(state=tk.DISABLED)
512
+
513
+ # ==================== HELPERS ====================
514
+ def _browse_var(self, var):
515
+ path = filedialog.askdirectory()
516
+ if path:
517
+ var.set(path)
518
+
519
+ def run(self):
520
+ self.root.mainloop()
@@ -0,0 +1,12 @@
1
+ You are a project generator. Create a complete Python project.
2
+
3
+ OUTPUT FORMAT:
4
+ 1. JSON: {"project":"<name>","mode":"create","files":[{"path":"<path>","action":"write","content":"<content>"}]}
5
+ 2. Convert JSON to UTF-8 bytes -> hex string (lowercase)
6
+ 3. Prepend: APBPACK1
7
+ 4. Output ONLY: APBPACK1<hex>
8
+
9
+ No markdown. No explanations. Only the hex string.
10
+
11
+ PROJECT:
12
+ {description}
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-project-builder
3
+ Version: 2.0.0
4
+ Summary: Fast Python project creation with AI
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fpdf2>=2.7.0
8
+
9
+ # AI Project Builder
10
+
11
+ Create Python projects instantly using AI.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pipx install . --system-site-packages
@@ -0,0 +1,19 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/ai_project_builder/__init__.py
4
+ src/ai_project_builder/__main__.py
5
+ src/ai_project_builder.egg-info/PKG-INFO
6
+ src/ai_project_builder.egg-info/SOURCES.txt
7
+ src/ai_project_builder.egg-info/dependency_links.txt
8
+ src/ai_project_builder.egg-info/entry_points.txt
9
+ src/ai_project_builder.egg-info/requires.txt
10
+ src/ai_project_builder.egg-info/top_level.txt
11
+ src/ai_project_builder/core/__init__.py
12
+ src/ai_project_builder/core/decoder.py
13
+ src/ai_project_builder/core/encoder.py
14
+ src/ai_project_builder/core/escapes.py
15
+ src/ai_project_builder/core/project_io.py
16
+ src/ai_project_builder/core/report.py
17
+ src/ai_project_builder/gui/__init__.py
18
+ src/ai_project_builder/gui/app.py
19
+ src/ai_project_builder/prompt/base_prompt.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ apb = ai_project_builder.__main__:main
@@ -0,0 +1 @@
1
+ ai_project_builder