synapseForge 0.1.24.dev2__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.
- pipeline/__init__.py +1 -0
- pipeline/init/__init__.py +7 -0
- pipeline/init/__main__.py +10 -0
- pipeline/init/config_handler.py +35 -0
- pipeline/init/input_handler.py +124 -0
- pipeline/init/logo_handler.py +33 -0
- pipeline/init/main.py +151 -0
- pipeline/init/placeholder_handler.py +162 -0
- pipeline/init/template_handler.py +89 -0
- pipeline/init/venv_handler.py +68 -0
- pipeline/launch/__init__.py +1 -0
- pipeline/launch/forge.py +701 -0
- pipeline/launch/templates/launcher.py +79 -0
- pipeline/template.zip +0 -0
- synapseforge/__init__.py +3 -0
- synapseforge/__main__.py +6 -0
- synapseforge/cli/__init__.py +1 -0
- synapseforge/cli/main.py +390 -0
- synapseforge/tk/__init__.py +1 -0
- synapseforge/tk/colors_app.py +305 -0
- synapseforge/tk/init_app.py +447 -0
- synapseforge/tk/logo.ico +0 -0
- synapseforge/tk/logo.png +0 -0
- synapseforge-0.1.24.dev2.dist-info/METADATA +187 -0
- synapseforge-0.1.24.dev2.dist-info/RECORD +29 -0
- synapseforge-0.1.24.dev2.dist-info/WHEEL +5 -0
- synapseforge-0.1.24.dev2.dist-info/entry_points.txt +2 -0
- synapseforge-0.1.24.dev2.dist-info/licenses/LICENSE +201 -0
- synapseforge-0.1.24.dev2.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""tkinter GUI for synapseforge init — replaces terminal get_user_input()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import re
|
|
7
|
+
import threading
|
|
8
|
+
import tkinter as tk
|
|
9
|
+
from tkinter import colorchooser, filedialog, messagebox, ttk
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import ctypes
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
_HERE = Path(__file__).resolve().parent
|
|
16
|
+
_ICO_PATH = _HERE / "logo.ico"
|
|
17
|
+
_LOGO_PNG_PATH = _HERE / "logo.png"
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from PIL import Image, ImageTk
|
|
21
|
+
except ImportError: # pragma: no cover
|
|
22
|
+
Image = None # type: ignore[assignment]
|
|
23
|
+
ImageTk = None
|
|
24
|
+
|
|
25
|
+
_HEX_RE = re.compile(r"^#[0-9a-fA-F]{6}$")
|
|
26
|
+
|
|
27
|
+
COLOR_TAB_FIELDS = [
|
|
28
|
+
("primary", "Color principal: botón de enviar, burbuja y avatar del asistente, barra de actividad, opción seleccionada del menú, enlaces de las respuestas y puntos de “escribiendo…”"),
|
|
29
|
+
("secondary", "Color de los detalles suaves: borde que se ilumina al hacer clic en un campo, anillo de la conversación seleccionada, bordes de las tarjetas y cursor de escritura"),
|
|
30
|
+
("primary_text", "Color del texto e íconos que van sobre el color principal: flecha de enviar, texto de botones, ícono del avatar y texto de la burbuja del asistente"),
|
|
31
|
+
("gradient_secondary", "Color final del degradé de los botones y del avatar del asistente (el inicio es el color principal)"),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ──────────────────────────────────────────────────────────────
|
|
36
|
+
# Fijar el AppUserModelID ANTES de crear cualquier ventana
|
|
37
|
+
# ──────────────────────────────────────────────────────────────
|
|
38
|
+
def _set_app_user_model_id() -> None:
|
|
39
|
+
if sys.platform != "win32":
|
|
40
|
+
return
|
|
41
|
+
try:
|
|
42
|
+
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
|
|
43
|
+
"synapseforge.init.1"
|
|
44
|
+
)
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
_set_app_user_model_id()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class InitApp:
|
|
52
|
+
"""GUI for collecting project configuration for synapseforge init."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, target_dir: str) -> None:
|
|
55
|
+
self.target_dir = target_dir
|
|
56
|
+
self.result: Optional[Dict[str, Any]] = None
|
|
57
|
+
|
|
58
|
+
# Crear la ventana
|
|
59
|
+
self.root = tk.Tk()
|
|
60
|
+
self.root.title("synapseForge — Init")
|
|
61
|
+
self.root.resizable(False, False)
|
|
62
|
+
|
|
63
|
+
# ── Configurar el ícono (con retraso para asegurar que la ventana esté lista) ──
|
|
64
|
+
self.root.after(100, self._set_icon)
|
|
65
|
+
|
|
66
|
+
# ── Logo header (desde archivo local) ────────────────────────
|
|
67
|
+
self._load_logo_local()
|
|
68
|
+
|
|
69
|
+
# ── Notebook (tabs) ──────────────────────────────────────────
|
|
70
|
+
self.notebook = ttk.Notebook(self.root)
|
|
71
|
+
self.notebook.pack(fill="both", expand=True, padx=10, pady=(0, 10))
|
|
72
|
+
|
|
73
|
+
self._build_project_tab()
|
|
74
|
+
self._build_logos_tab()
|
|
75
|
+
self._build_colors_tab()
|
|
76
|
+
|
|
77
|
+
# ── Bottom frame (progress + buttons) ────────────────────────
|
|
78
|
+
bottom = ttk.Frame(self.root)
|
|
79
|
+
bottom.pack(fill="x", padx=10, pady=(0, 10))
|
|
80
|
+
|
|
81
|
+
self.progress = ttk.Progressbar(bottom, mode="indeterminate", length=400)
|
|
82
|
+
self.progress.pack(side="left", fill="x", expand=True, padx=(0, 10))
|
|
83
|
+
|
|
84
|
+
self.btn_cancel = ttk.Button(bottom, text="Cancelar", command=self._on_cancel)
|
|
85
|
+
self.btn_cancel.pack(side="right", padx=(5, 0))
|
|
86
|
+
|
|
87
|
+
self.btn_submit = ttk.Button(bottom, text="Crear Proyecto", command=self._on_submit)
|
|
88
|
+
self.btn_submit.pack(side="right")
|
|
89
|
+
|
|
90
|
+
# ── Center window ────────────────────────────────────────────
|
|
91
|
+
self._center(640, 800)
|
|
92
|
+
|
|
93
|
+
# ──────────────────────────────────────────────────────────────────
|
|
94
|
+
# Configuración robusta del ícono usando ctypes
|
|
95
|
+
# ──────────────────────────────────────────────────────────────────
|
|
96
|
+
def _set_icon(self) -> None:
|
|
97
|
+
"""Asigna el ícono de la ventana y de la barra de tareas usando ctypes."""
|
|
98
|
+
if not _ICO_PATH.is_file():
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# 1. iconbitmap (funciona para la ventana)
|
|
103
|
+
self.root.iconbitmap(str(_ICO_PATH.resolve()))
|
|
104
|
+
|
|
105
|
+
# 2. Forzar actualización de la barra de tareas con ctypes
|
|
106
|
+
hwnd = self.root.winfo_id()
|
|
107
|
+
user32 = ctypes.windll.user32
|
|
108
|
+
# Cargar el ícono desde el archivo
|
|
109
|
+
hicon = user32.LoadImageW(
|
|
110
|
+
0,
|
|
111
|
+
str(_ICO_PATH.resolve()),
|
|
112
|
+
1, # IMAGE_ICON
|
|
113
|
+
0, 0,
|
|
114
|
+
0x00000010 # LR_LOADFROMFILE
|
|
115
|
+
)
|
|
116
|
+
if hicon:
|
|
117
|
+
# GCL_HICON = -14, GCL_HICONSM = -34
|
|
118
|
+
user32.SetClassLongW(hwnd, -14, hicon)
|
|
119
|
+
user32.SetClassLongW(hwnd, -34, hicon)
|
|
120
|
+
# WM_SETICON = 0x0080, ICON_BIG = 0, ICON_SMALL = 1
|
|
121
|
+
user32.SendMessageW(hwnd, 0x0080, 0, hicon)
|
|
122
|
+
user32.SendMessageW(hwnd, 0x0080, 1, hicon)
|
|
123
|
+
|
|
124
|
+
except Exception:
|
|
125
|
+
pass # Silencioso si falla
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
# Logo (cargado desde archivo local)
|
|
129
|
+
# ------------------------------------------------------------------
|
|
130
|
+
def _load_logo_local(self) -> None:
|
|
131
|
+
"""Carga el logo desde logo_transparente.png (local) y lo muestra en la ventana."""
|
|
132
|
+
if Image is None or ImageTk is None:
|
|
133
|
+
return
|
|
134
|
+
if not _LOGO_PNG_PATH.is_file():
|
|
135
|
+
return
|
|
136
|
+
try:
|
|
137
|
+
pil = Image.open(_LOGO_PNG_PATH)
|
|
138
|
+
pil.thumbnail((150, 150), Image.LANCZOS)
|
|
139
|
+
self._logo_img = ImageTk.PhotoImage(pil)
|
|
140
|
+
lbl = tk.Label(self.root, image=self._logo_img)
|
|
141
|
+
lbl.pack(pady=(10, 5))
|
|
142
|
+
except Exception:
|
|
143
|
+
pass # silencioso si falla
|
|
144
|
+
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
# Tab 1: Project info
|
|
147
|
+
# ------------------------------------------------------------------
|
|
148
|
+
def _build_project_tab(self) -> None:
|
|
149
|
+
tab = ttk.Frame(self.notebook, padding=15)
|
|
150
|
+
self.notebook.add(tab, text="Proyecto")
|
|
151
|
+
|
|
152
|
+
fields: list[tuple[str, str, bool]] = [
|
|
153
|
+
("empresa", "Nombre de la empresa desarrolladora", True),
|
|
154
|
+
("owner", "Owner del repo (usuario de GitHub)", True),
|
|
155
|
+
("legal", "Nombre legal / razón social", True),
|
|
156
|
+
("repo", "Nombre del repo", True),
|
|
157
|
+
("cliente", "Nombre del cliente", True),
|
|
158
|
+
("descripcion", "Descripción del proyecto", True),
|
|
159
|
+
("tarea", "Nombre de la tarea / rubro", True),
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
self._entries: Dict[str, tk.Entry] = {}
|
|
163
|
+
for i, (key, label, required) in enumerate(fields):
|
|
164
|
+
lbl_text = label + " *" if required else label
|
|
165
|
+
lbl = ttk.Label(tab, text=lbl_text)
|
|
166
|
+
lbl.grid(row=i, column=0, sticky="w", pady=3, padx=(0, 10))
|
|
167
|
+
ent = ttk.Entry(tab, width=55)
|
|
168
|
+
ent.grid(row=i, column=1, pady=3)
|
|
169
|
+
self._entries[key] = ent
|
|
170
|
+
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
# Tab 2: Logos (file pickers)
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
def _build_logos_tab(self) -> None:
|
|
175
|
+
tab = ttk.Frame(self.notebook, padding=15)
|
|
176
|
+
self.notebook.add(tab, text="Logos")
|
|
177
|
+
|
|
178
|
+
# ── Logo empresa (required) ──────────────────────────────────
|
|
179
|
+
ttk.Label(tab, text="Logo de la empresa (para README) *").grid(
|
|
180
|
+
row=0, column=0, sticky="w", pady=3
|
|
181
|
+
)
|
|
182
|
+
self._logo_path_var = tk.StringVar()
|
|
183
|
+
ttk.Entry(tab, textvariable=self._logo_path_var, width=50).grid(
|
|
184
|
+
row=0, column=1, padx=(0, 5), pady=3
|
|
185
|
+
)
|
|
186
|
+
ttk.Button(tab, text="Examinar…", command=self._browse_logo).grid(
|
|
187
|
+
row=0, column=2, pady=3
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# ── Logo cliente (optional) ──────────────────────────────────
|
|
191
|
+
ttk.Label(tab, text="Logo del cliente (para la app)").grid(
|
|
192
|
+
row=1, column=0, sticky="w", pady=(15, 3)
|
|
193
|
+
)
|
|
194
|
+
self._logo_cliente_var = tk.StringVar()
|
|
195
|
+
ttk.Entry(tab, textvariable=self._logo_cliente_var, width=50).grid(
|
|
196
|
+
row=1, column=1, padx=(0, 5), pady=(15, 3)
|
|
197
|
+
)
|
|
198
|
+
ttk.Button(tab, text="Examinar…", command=self._browse_logo_cliente).grid(
|
|
199
|
+
row=1, column=2, pady=(15, 3)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def _browse_logo(self) -> None:
|
|
203
|
+
path = filedialog.askopenfilename(
|
|
204
|
+
title="Seleccionar logo de la empresa",
|
|
205
|
+
filetypes=[("Imágenes", "*.png *.jpg *.jpeg *.gif *.bmp"), ("Todos", "*.*")],
|
|
206
|
+
)
|
|
207
|
+
if path:
|
|
208
|
+
self._logo_path_var.set(path)
|
|
209
|
+
|
|
210
|
+
def _browse_logo_cliente(self) -> None:
|
|
211
|
+
path = filedialog.askopenfilename(
|
|
212
|
+
title="Seleccionar logo del cliente",
|
|
213
|
+
filetypes=[("Imágenes", "*.png *.jpg *.jpeg *.gif *.bmp"), ("Todos", "*.*")],
|
|
214
|
+
)
|
|
215
|
+
if path:
|
|
216
|
+
self._logo_cliente_var.set(path)
|
|
217
|
+
|
|
218
|
+
# ------------------------------------------------------------------
|
|
219
|
+
# Tab 3: Colors (optional hex fields with color picker)
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
def _build_colors_tab(self) -> None:
|
|
222
|
+
tab = ttk.Frame(self.notebook, padding=15)
|
|
223
|
+
self.notebook.add(tab, text="Colores")
|
|
224
|
+
|
|
225
|
+
ttk.Label(
|
|
226
|
+
tab,
|
|
227
|
+
text="Colores del proyecto (obligatorios):",
|
|
228
|
+
font=("", 10, "bold"),
|
|
229
|
+
).grid(row=0, column=0, columnspan=4, sticky="w", pady=(0, 10))
|
|
230
|
+
|
|
231
|
+
self._color_entries: Dict[str, tk.Entry] = {}
|
|
232
|
+
self._color_previews: Dict[str, tk.Canvas] = {}
|
|
233
|
+
|
|
234
|
+
for i, (key, desc) in enumerate(COLOR_TAB_FIELDS, start=1):
|
|
235
|
+
row = i * 2
|
|
236
|
+
|
|
237
|
+
# Explanation (wraps so it doesn't push the selector out of view)
|
|
238
|
+
ttk.Label(tab, text=desc, wraplength=420, justify="left").grid(
|
|
239
|
+
row=row, column=0, columnspan=4, sticky="w", pady=(6, 0), padx=(0, 8)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Preview square (16×16)
|
|
243
|
+
cv = tk.Canvas(tab, width=18, height=18, highlightthickness=1,
|
|
244
|
+
highlightbackground="#ccc")
|
|
245
|
+
cv.grid(row=row + 1, column=0, pady=(4, 6), padx=(0, 4))
|
|
246
|
+
self._color_previews[key] = cv
|
|
247
|
+
|
|
248
|
+
ent = ttk.Entry(tab, width=12)
|
|
249
|
+
ent.grid(row=row + 1, column=1, pady=(4, 6), padx=(0, 4))
|
|
250
|
+
self._color_entries[key] = ent
|
|
251
|
+
|
|
252
|
+
# Bind entry change → update preview
|
|
253
|
+
ent.bind("<KeyRelease>", lambda _e, k=key: self._update_preview(k))
|
|
254
|
+
|
|
255
|
+
ttk.Button(tab, text="Seleccionar", command=lambda k=key: self._pick_color(k)).grid(
|
|
256
|
+
row=row + 1, column=2, pady=(4, 6), sticky="w"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# ── Gradient toggle ──────────────────────────────────────────
|
|
260
|
+
self._usar_gradiente_var = tk.BooleanVar(value=True)
|
|
261
|
+
cb = ttk.Checkbutton(
|
|
262
|
+
tab,
|
|
263
|
+
text="Usar degradé en botones y avatar",
|
|
264
|
+
variable=self._usar_gradiente_var,
|
|
265
|
+
command=self._toggle_gradient_fields,
|
|
266
|
+
)
|
|
267
|
+
cb.grid(row=len(COLOR_TAB_FIELDS) * 2 + 2, column=0, columnspan=4, sticky="w", pady=(12, 0))
|
|
268
|
+
|
|
269
|
+
ttk.Label(
|
|
270
|
+
tab,
|
|
271
|
+
text="Si lo desactivás, botones y avatar usan el color principal liso.",
|
|
272
|
+
font=("", 8, "italic"),
|
|
273
|
+
foreground="#888",
|
|
274
|
+
).grid(row=len(COLOR_TAB_FIELDS) * 2 + 3, column=0, columnspan=4, sticky="w")
|
|
275
|
+
|
|
276
|
+
def _pick_color(self, key: str) -> None:
|
|
277
|
+
"""Open OS color chooser, fill entry and update preview."""
|
|
278
|
+
result = colorchooser.askcolor(
|
|
279
|
+
title=key,
|
|
280
|
+
parent=self.root,
|
|
281
|
+
)
|
|
282
|
+
if result and result[1]: # result is ((R,G,B), "#RRGGBB")
|
|
283
|
+
hex_val = result[1]
|
|
284
|
+
self._color_entries[key].delete(0, tk.END)
|
|
285
|
+
self._color_entries[key].insert(0, hex_val)
|
|
286
|
+
self._update_preview(key)
|
|
287
|
+
|
|
288
|
+
def _update_preview(self, key: str) -> None:
|
|
289
|
+
"""Update the preview square for the given color key."""
|
|
290
|
+
cv = self._color_previews[key]
|
|
291
|
+
raw = self._color_entries[key].get().strip()
|
|
292
|
+
cv.delete("all")
|
|
293
|
+
if raw and _HEX_RE.match(raw):
|
|
294
|
+
cv.config(bg=raw)
|
|
295
|
+
else:
|
|
296
|
+
cv.config(bg="#ffffff")
|
|
297
|
+
|
|
298
|
+
def _toggle_gradient_fields(self) -> None:
|
|
299
|
+
"""Enable/disable gradient_secondary picker based on checkbox."""
|
|
300
|
+
state = "normal" if self._usar_gradiente_var.get() else "disabled"
|
|
301
|
+
key = "gradient_secondary"
|
|
302
|
+
if key in self._color_entries:
|
|
303
|
+
self._color_entries[key].config(state=state)
|
|
304
|
+
if key in self._color_previews:
|
|
305
|
+
self._color_previews[key].config(highlightbackground="#ccc" if state == "normal" else "#eee")
|
|
306
|
+
|
|
307
|
+
# ------------------------------------------------------------------
|
|
308
|
+
# Collect + validate
|
|
309
|
+
# ------------------------------------------------------------------
|
|
310
|
+
def _collect_config(self) -> Optional[Dict[str, Any]]:
|
|
311
|
+
"""Read all fields and return config dict, or None if validation fails."""
|
|
312
|
+
|
|
313
|
+
# ── Required text fields ──────────────────────────────────────
|
|
314
|
+
required = ["empresa", "owner", "legal", "repo", "cliente", "descripcion", "tarea"]
|
|
315
|
+
config: Dict[str, Any] = {}
|
|
316
|
+
for key in required:
|
|
317
|
+
val = self._entries[key].get().strip()
|
|
318
|
+
if not val:
|
|
319
|
+
messagebox.showwarning("Campo requerido",
|
|
320
|
+
f"'{key}' es obligatorio.", parent=self.root)
|
|
321
|
+
return None
|
|
322
|
+
config[key] = val
|
|
323
|
+
|
|
324
|
+
# ── Logo empresa ──────────────────────────────────────────────
|
|
325
|
+
logo_path = self._logo_path_var.get().strip()
|
|
326
|
+
if not logo_path:
|
|
327
|
+
messagebox.showwarning("Campo requerido",
|
|
328
|
+
"El logo de la empresa es obligatorio.", parent=self.root)
|
|
329
|
+
return None
|
|
330
|
+
from pathlib import Path
|
|
331
|
+
logo_resolved = Path(logo_path).resolve()
|
|
332
|
+
if not logo_resolved.is_file():
|
|
333
|
+
messagebox.showwarning("Archivo no encontrado",
|
|
334
|
+
f"No se encontró: {logo_resolved}", parent=self.root)
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
config["logo"] = {
|
|
338
|
+
"path": str(logo_resolved),
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# ── Logo cliente ──────────────────────────────────────────────
|
|
342
|
+
logo_cliente = self._logo_cliente_var.get().strip()
|
|
343
|
+
config["logo_cliente"] = logo_cliente or None
|
|
344
|
+
|
|
345
|
+
# ── Colors (required) ─────────────────────────────────────────
|
|
346
|
+
colors: Dict[str, str] = {}
|
|
347
|
+
usar_gradiente = self._usar_gradiente_var.get()
|
|
348
|
+
colors["usar_gradiente"] = usar_gradiente
|
|
349
|
+
for key, _desc in COLOR_TAB_FIELDS:
|
|
350
|
+
# Skip gradient_secondary if gradient toggle is off
|
|
351
|
+
if key == "gradient_secondary" and not usar_gradiente:
|
|
352
|
+
colors[key] = colors.get("primary", "#000000")
|
|
353
|
+
continue
|
|
354
|
+
val = self._color_entries[key].get().strip()
|
|
355
|
+
if not val:
|
|
356
|
+
messagebox.showwarning(
|
|
357
|
+
"Color requerido",
|
|
358
|
+
f"'{_desc}' es obligatorio.",
|
|
359
|
+
parent=self.root,
|
|
360
|
+
)
|
|
361
|
+
return None
|
|
362
|
+
if not _HEX_RE.match(val):
|
|
363
|
+
messagebox.showwarning(
|
|
364
|
+
"Color inválido",
|
|
365
|
+
f"{key}: '{val}' no es un color hex válido (#RRGGBB).",
|
|
366
|
+
parent=self.root,
|
|
367
|
+
)
|
|
368
|
+
return None
|
|
369
|
+
colors[key] = val
|
|
370
|
+
config["colors"] = colors
|
|
371
|
+
|
|
372
|
+
return config
|
|
373
|
+
|
|
374
|
+
def _on_submit(self) -> None:
|
|
375
|
+
config = self._collect_config()
|
|
376
|
+
if config is None:
|
|
377
|
+
return # validation failed
|
|
378
|
+
|
|
379
|
+
# Disable UI and start progress
|
|
380
|
+
self._set_ui_enabled(False)
|
|
381
|
+
self.progress.start(15)
|
|
382
|
+
|
|
383
|
+
# Run pipeline in daemon thread
|
|
384
|
+
t = threading.Thread(target=self._run_pipeline, args=(config,), daemon=True)
|
|
385
|
+
t.start()
|
|
386
|
+
|
|
387
|
+
def _run_pipeline(self, config: Dict[str, Any]) -> None:
|
|
388
|
+
"""Execute init pipeline in background thread."""
|
|
389
|
+
try:
|
|
390
|
+
from pipeline.init.main import run
|
|
391
|
+
run(self.target_dir, config=config)
|
|
392
|
+
self.result = config
|
|
393
|
+
self.root.after(0, self._on_success)
|
|
394
|
+
except Exception as exc:
|
|
395
|
+
# Bind the message now: ``exc`` is deleted when the except block
|
|
396
|
+
# exits, before the deferred callback runs.
|
|
397
|
+
msg = str(exc)
|
|
398
|
+
self.root.after(0, lambda: self._on_error(msg))
|
|
399
|
+
|
|
400
|
+
def _on_success(self) -> None:
|
|
401
|
+
self.progress.stop()
|
|
402
|
+
self.root.destroy()
|
|
403
|
+
|
|
404
|
+
def _on_error(self, msg: str) -> None:
|
|
405
|
+
self.progress.stop()
|
|
406
|
+
messagebox.showerror("Error", msg, parent=self.root)
|
|
407
|
+
self._set_ui_enabled(True)
|
|
408
|
+
|
|
409
|
+
def _on_cancel(self) -> None:
|
|
410
|
+
self.result = None
|
|
411
|
+
self.root.destroy()
|
|
412
|
+
|
|
413
|
+
def _set_ui_enabled(self, enabled: bool) -> None:
|
|
414
|
+
state = "normal" if enabled else "disabled"
|
|
415
|
+
self.btn_submit.config(state=state)
|
|
416
|
+
self.btn_cancel.config(state=state)
|
|
417
|
+
for ent in self._entries.values():
|
|
418
|
+
ent.config(state=state)
|
|
419
|
+
# notebooks aren't easily disabled, skip for UX
|
|
420
|
+
|
|
421
|
+
# ------------------------------------------------------------------
|
|
422
|
+
# Window utils
|
|
423
|
+
# ------------------------------------------------------------------
|
|
424
|
+
def _center(self, w: int, h: int) -> None:
|
|
425
|
+
sw = self.root.winfo_screenwidth()
|
|
426
|
+
sh = self.root.winfo_screenheight()
|
|
427
|
+
x = (sw - w) // 2
|
|
428
|
+
y = (sh - h) // 2
|
|
429
|
+
self.root.geometry(f"{w}x{h}+{x}+{y}")
|
|
430
|
+
|
|
431
|
+
# ------------------------------------------------------------------
|
|
432
|
+
# Public entry point
|
|
433
|
+
# ------------------------------------------------------------------
|
|
434
|
+
@staticmethod
|
|
435
|
+
def launch(target_dir: str) -> Optional[Dict[str, Any]]:
|
|
436
|
+
"""Open the init GUI and return the config dict (or None if cancelled).
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
target_dir: Absolute path to the target project directory.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Config dictionary matching ``input_handler.get_user_input()``
|
|
443
|
+
format, or ``None`` if the user cancelled.
|
|
444
|
+
"""
|
|
445
|
+
app = InitApp(target_dir)
|
|
446
|
+
app.root.mainloop()
|
|
447
|
+
return app.result
|
synapseforge/tk/logo.ico
ADDED
|
Binary file
|
synapseforge/tk/logo.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: synapseForge
|
|
3
|
+
Version: 0.1.24.dev2
|
|
4
|
+
Summary: AI agent project scaffolding & distribution CLI
|
|
5
|
+
Author-email: "synapse.ai" <developer@synapseaihub.com.ar>
|
|
6
|
+
Maintainer-email: "synapse.ai" <developer@synapseaihub.com.ar>
|
|
7
|
+
License: Apache-2.0
|
|
8
|
+
Project-URL: Homepage, https://www.synapseaihub.com.ar
|
|
9
|
+
Project-URL: Repository, https://github.com/synapse-ai-hub/synapseForge
|
|
10
|
+
Project-URL: Issues, https://github.com/synapse-ai-hub/synapseForge/issues
|
|
11
|
+
Keywords: ai-agents,llm,framework,cli,fastapi,react,mcp,tool-calling
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: Pillow>=10.0.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: ruff>=0.10.0; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.15.0; extra == "dev"
|
|
26
|
+
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pre-commit>=4.0.0; extra == "dev"
|
|
28
|
+
Provides-Extra: build
|
|
29
|
+
Requires-Dist: pyinstaller>=6.0.0; extra == "build"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="https://github.com/synapse-ai-hub/sources/raw/main/logo_transparente.png" alt="Logo" width="150">
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
<h1 align="center">
|
|
39
|
+
<img src="https://github.com/synapse-ai-hub/sources/raw/main/forge.png" alt="synapseForge" width="420">
|
|
40
|
+
</h1>
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
<p align="center">
|
|
45
|
+
<a href="./LICENSE">
|
|
46
|
+
<img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License: Apache 2.0" />
|
|
47
|
+
</a>
|
|
48
|
+
|
|
49
|
+
<a href="https://link.mercadopago.com.ar/synapseforge">
|
|
50
|
+
<img src="https://github.com/synapse-ai-hub/sources/raw/main/badges/mercadopago-support.svg" alt="Support this project" />
|
|
51
|
+
</a>
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<h3 align="center">CLI to scaffold and ship full-stack AI agent projects (FastAPI + React/Vite/TS)</h3>
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Description
|
|
61
|
+
|
|
62
|
+
**synapseForge** is a PyPI package that provides a CLI to scaffold full-stack AI agent projects from scratch — backend (FastAPI), frontend (React/Vite/TypeScript), branding (logo, .ico, color palette), dependencies, and a self-contained distribution build.
|
|
63
|
+
|
|
64
|
+
The generated project includes:
|
|
65
|
+
|
|
66
|
+
- **Agent Framework**: AgentLoop with native tool calling, tools registry (native + external + MCP), sessions (SQLite WAL), per-agent permissions, skills and sub-agent delegation
|
|
67
|
+
- **Multi-provider LLM**: LOCAL (Ollama), Groq, Google Gemini and OpenRouter — cloud API keys managed from the config panel, validated against each provider's API and stored encrypted in SQLite
|
|
68
|
+
- **RAG knowledge base**: ChromaDB vector collections with cloud embeddings via OpenRouter; upload files and web pages, cosine-similarity search
|
|
69
|
+
- **LLM-assisted creation**: standalone interfaces to generate skills, tools and agents through an iterative interview (with real tools enabled), with ephemeral cloud model selection per task
|
|
70
|
+
- **Scheduled tasks**: user-defined tasks (description + time + weekdays) managed from the header Agenda or via Telegram; the backend runs them with the selected model and notifies the result in the UI bell and on Telegram
|
|
71
|
+
- **Telegram bot**: remote control that bridges messages to the agent through the web UI (commands, voice transcription, attachments)
|
|
72
|
+
- **Frontend**: chat with SSE streaming, config panel, sessions sidebar, context-window gauge, metrics dashboard
|
|
73
|
+
- **Docker** support and **desktop app mode** (heartbeat watchdog + shutdown endpoint)
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install synapseForge
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Package dependencies: `Pillow` (.ico generation) — everything else is project-level.
|
|
84
|
+
|
|
85
|
+
## Requirements
|
|
86
|
+
|
|
87
|
+
| Tool | Version | Needed for |
|
|
88
|
+
|------|---------|------------|
|
|
89
|
+
| Python | 3.12+ | `init`, `launch`, `run`, `colors` |
|
|
90
|
+
| Node.js | 20+ | `launch` (frontend build), `run` (dev server) |
|
|
91
|
+
| Docker | 20+ | Optional: containerized deployment |
|
|
92
|
+
|
|
93
|
+
**LLM provider (required):** at least one cloud API key is needed to use the app — [OpenRouter](https://openrouter.ai/settings/keys), [Google Gemini](https://aistudio.google.com/apikey) or [Groq](https://console.groq.com/keys) all offer free tiers. Keys are loaded from the in-app config panel (**Providers**) on first launch; nothing else has to be installed.
|
|
94
|
+
|
|
95
|
+
> The **knowledge base** feature specifically requires an **OpenRouter** key (free tier works). Without it, that section stays disabled — everything else runs normally.
|
|
96
|
+
|
|
97
|
+
**Ollama (optional):** local models are supported but not required. Install Ollama only if you want to run models locally.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Quick Start
|
|
102
|
+
|
|
103
|
+
### Create a new project
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
synapseforge init my-project
|
|
107
|
+
cd my-project
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Interactive GUI pipeline (project data, logos, colors).
|
|
111
|
+
|
|
112
|
+
### Run in development
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
synapseforge run ./my-project
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Requires the project venv to be activated (`VIRTUAL_ENV`). Starts backend + frontend dev servers and opens the browser. Ctrl+C stops both.
|
|
119
|
+
|
|
120
|
+
### Build a distributable
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
synapseforge launch -p ./my-project -n "MyApp"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Builds the frontend, bundles embedded Python and packages everything into a self-contained zip ready to deliver. By default the backend ships as `.py` sources; pass `-c` / `--compile` to compile it to `.pyc`. Other options: `--skip-frontend`, `--no-embed`.
|
|
127
|
+
|
|
128
|
+
### Edit colors at runtime (no rebuild)
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
synapseforge colors ./my-project
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
GUI editor for `frontend/public/colors.json`. Refresh the browser (F5) to see changes instantly.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## CLI Reference
|
|
139
|
+
|
|
140
|
+
| Command | Description |
|
|
141
|
+
|---------|-------------|
|
|
142
|
+
| `synapseforge init [dir]` | Scaffold a project from bundled template (GUI) |
|
|
143
|
+
| `synapseforge launch -p <path> -n <exe> [--skip-frontend] [--no-embed] [-c]` | Build self-contained distribution zip (`-c` compiles backend to `.pyc`, default ships `.py`) |
|
|
144
|
+
| `synapseforge colors [dir]` | Edit `frontend/public/colors.json` via GUI (live reload) |
|
|
145
|
+
| `synapseforge run [dir]` | Start uvicorn + npm dev servers, open browser (venv must be active) |
|
|
146
|
+
| `synapseforge --help` | Show global help |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Tech Stack
|
|
151
|
+
|
|
152
|
+
[](https://www.python.org/)
|
|
153
|
+
[](https://fastapi.tiangolo.com/)
|
|
154
|
+
[](https://www.sqlite.org/)
|
|
155
|
+
[](https://www.trychroma.com/)
|
|
156
|
+
[](https://react.dev/)
|
|
157
|
+
[](https://www.typescriptlang.org/)
|
|
158
|
+
[](https://vite.dev/)
|
|
159
|
+
[](https://tailwindcss.com/)
|
|
160
|
+
[](https://ui.shadcn.com/)
|
|
161
|
+
[](https://nodejs.org/)
|
|
162
|
+
[](https://pyinstaller.org/)
|
|
163
|
+
[](https://www.docker.com/)
|
|
164
|
+
[](https://telegram.org/)
|
|
165
|
+
[](https://pypi.org/)
|
|
166
|
+
[](https://groq.com/)
|
|
167
|
+
[](https://ai.google.dev/)
|
|
168
|
+
[](https://openrouter.ai/)
|
|
169
|
+
[](https://ollama.com/)
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Support this project
|
|
174
|
+
|
|
175
|
+
synapseForge will always be free and open source. If you find it useful, consider supporting its development with a [donation via Mercado Pago](https://link.mercadopago.com.ar/synapseforge) — your donation goes directly into new features, fixes and better docs for everyone.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
Apache 2.0
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
Copyright (c) 2026 SYNASPE AI SAS
|
|
186
|
+
|
|
187
|
+
---
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
pipeline/__init__.py,sha256=LTNoAzYsOBPjlMBN535yd4Wgqeip82REd351GiRc_d0,70
|
|
2
|
+
pipeline/template.zip,sha256=ZP6s3j7plTbKOjT-4ACx5dGptf4N6FPefhQDgjgyKcU,443942
|
|
3
|
+
pipeline/init/__init__.py,sha256=whbnhhDnssVvRCEaqRzx84T1tTQxku0oEQAcrfY9NEg,204
|
|
4
|
+
pipeline/init/__main__.py,sha256=DmAGPXdmSm97TBIjFXmVkU1MEhibSF5V6JGchdV0n5c,217
|
|
5
|
+
pipeline/init/config_handler.py,sha256=xk5aUh0bpK5ohnTDjPTNYA0PYrOe0Z-lkLEY_UxFFbk,1341
|
|
6
|
+
pipeline/init/input_handler.py,sha256=Ue22816T_76prfUEsOO4ifjViGujvvc4pTkVduAT-3k,4934
|
|
7
|
+
pipeline/init/logo_handler.py,sha256=1Kd05nhz3jufAKgfemNvrYLQ1nvPpHDhWU0QCXOBYxM,1121
|
|
8
|
+
pipeline/init/main.py,sha256=Eiqqh9m4IidjEOqbviRc9PenUUxx6Unu096vdkpy3-E,5427
|
|
9
|
+
pipeline/init/placeholder_handler.py,sha256=xbvHiwfOxYn4366LznX0CiddiXs5qWJoNnQ76IJajwQ,5176
|
|
10
|
+
pipeline/init/template_handler.py,sha256=XH4ka6ilf0d_az9Nya3YuoMCqNI_nWk4E1idHLAjWQ8,3018
|
|
11
|
+
pipeline/init/venv_handler.py,sha256=rvugBdUcWU-xajIITfmxQGD2mKNPlXY4_2pKYy16fHU,2054
|
|
12
|
+
pipeline/launch/__init__.py,sha256=kTjcQgfeK-2CjUXJFZtFIVRTc1_ibT2pWBEpJK2QxfI,58
|
|
13
|
+
pipeline/launch/forge.py,sha256=xfDiFovkM_g7ipYFO4qhnL9wJKMLx3Np4aZ9YLEU8ho,24930
|
|
14
|
+
pipeline/launch/templates/launcher.py,sha256=pSrhLmyXdnCv0Tdae5GOWYqbOOf7CtVBD7jwute8J_Y,2077
|
|
15
|
+
synapseforge/__init__.py,sha256=WC0q7w92WA_KK0PU4ngoM0m6wG5-dYyXCLwPsz1ym48,99
|
|
16
|
+
synapseforge/__main__.py,sha256=c8L2gqOZGeF8pYZ0D3_SbfT_jzCoTRC6TE2aT70q_Mk,134
|
|
17
|
+
synapseforge/cli/__init__.py,sha256=tu3FwtlQJ_oU8TIXdFE5OLfANQMa0u7Yqtr--Dh3Zbc,51
|
|
18
|
+
synapseforge/cli/main.py,sha256=8DwozYeXU5sdp375AfychQIednKcrA3JjXctujVid8A,15808
|
|
19
|
+
synapseforge/tk/__init__.py,sha256=U8qSNZIc_98Q7ZJqTnrINgHADFfPq-RUjupaz2xDQ14,56
|
|
20
|
+
synapseforge/tk/colors_app.py,sha256=ROZanpzLQKw5y9867mKK_6S8MSXKBMMrPkYNbt6RfXs,13338
|
|
21
|
+
synapseforge/tk/init_app.py,sha256=d9qZhFQs8lJy5akeVxmq97uzUu44FY7gPSNdL_knYMg,19860
|
|
22
|
+
synapseforge/tk/logo.ico,sha256=Q46zWEsYbqfmcB_1HaRqZM9kyFORGGQaFQPn-r_D16w,43415
|
|
23
|
+
synapseforge/tk/logo.png,sha256=bLPNBk-kAhTBVlGraFr9o-abJJ98KKiolj33WcG2zUY,81279
|
|
24
|
+
synapseforge-0.1.24.dev2.dist-info/licenses/LICENSE,sha256=MutYgYox1x17FbtHn5lIxcBSgE3BsVqkfj0-JJMQhdQ,11550
|
|
25
|
+
synapseforge-0.1.24.dev2.dist-info/METADATA,sha256=cBgfvxqM-42LXSnr3TMh1cKwoOxfCSRYh-TirNYyqdE,8792
|
|
26
|
+
synapseforge-0.1.24.dev2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
27
|
+
synapseforge-0.1.24.dev2.dist-info/entry_points.txt,sha256=zNxPVdol1eNIZxZlXtq5oshdPiaWMtoL2kmdIKfASBs,60
|
|
28
|
+
synapseforge-0.1.24.dev2.dist-info/top_level.txt,sha256=U8Du3csIq81izghzQDWFFT1Q9nhUNEAGiPA-8BBL-r0,22
|
|
29
|
+
synapseforge-0.1.24.dev2.dist-info/RECORD,,
|