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.
@@ -0,0 +1,79 @@
1
+ """Launcher for {{EXE_NAME}}.
2
+
3
+ Generated by synapseForge — do not edit manually.
4
+ Uses embedded Python to run the FastAPI server.
5
+ Opens the browser automatically after startup.
6
+ """
7
+
8
+ import os
9
+ import subprocess
10
+ import sys
11
+ import threading
12
+ import time
13
+
14
+
15
+ # Project-specific values (injected by forge.py)
16
+ APP_MODULE = "{{APP_MODULE}}"
17
+ PORT = "{{PORT}}"
18
+ EXE_NAME = "{{EXE_NAME}}"
19
+
20
+
21
+ def _open_browser(url: str, delay: int = 4) -> None:
22
+ """Open the browser after a short delay to let the server start."""
23
+ import webbrowser
24
+ time.sleep(delay)
25
+ webbrowser.open(url)
26
+
27
+
28
+ def main() -> None:
29
+ # Determine project root: exe location when frozen
30
+ if getattr(sys, "frozen", False):
31
+ base_dir = os.path.dirname(sys.executable)
32
+ else:
33
+ # Development: go up from __forge_build__/
34
+ base_dir = os.path.dirname(
35
+ os.path.dirname(os.path.abspath(__file__))
36
+ )
37
+ os.chdir(base_dir)
38
+
39
+ python_dir = os.path.join(base_dir, "python")
40
+ python_exe = os.path.join(python_dir, "python.exe")
41
+
42
+ # Verify embedded Python exists
43
+ if not os.path.isfile(python_exe):
44
+ if hasattr(sys, "stdin") and sys.stdin is not None:
45
+ input(
46
+ f"Error: No se encuentra Python embebido en:\n"
47
+ f" {python_exe}\n\n"
48
+ "Asegurate de que la carpeta python/ existe.\n\n"
49
+ "Presiona Enter para salir..."
50
+ )
51
+ sys.exit(1)
52
+
53
+ # Open browser in background
54
+ threading.Thread(
55
+ target=_open_browser,
56
+ args=("http://localhost:" + PORT,),
57
+ daemon=True,
58
+ ).start()
59
+
60
+ # Run uvicorn via embedded Python (no hardcoded paths)
61
+ cmd = [
62
+ python_exe, "-m", "uvicorn",
63
+ APP_MODULE,
64
+ "--host", "localhost",
65
+ "--port", PORT,
66
+ ]
67
+ creation_flags = 0
68
+ if sys.platform == "win32":
69
+ creation_flags = subprocess.CREATE_NO_WINDOW
70
+ proc = subprocess.run(cmd, shell=False, creationflags=creation_flags)
71
+
72
+ if proc.returncode != 0:
73
+ pass
74
+ else:
75
+ pass
76
+
77
+
78
+ if __name__ == "__main__":
79
+ main()
pipeline/template.zip ADDED
Binary file
@@ -0,0 +1,3 @@
1
+ """synapseForge CLI — scaffold and distribute AI agent projects."""
2
+
3
+ __version__ = "0.1.24.dev2"
@@ -0,0 +1,6 @@
1
+ """Allow running ``python -m synapseforge`` (delegates to CLI)."""
2
+
3
+ from .cli.main import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1 @@
1
+ """CLI package — init and launch subcommands."""
@@ -0,0 +1,390 @@
1
+ """CLI entry point — ``synapseforge init``, ``synapseforge launch``, ``synapseforge colors``, ``synapseforge run``.
2
+
3
+ Installed via ``pyproject.toml`` entry point::
4
+
5
+ synapseforge init [target_dir]
6
+ synapseforge launch [-p PATH] -n NAME [--skip-frontend] [--no-embed] [-c]
7
+ synapseforge colors [project_dir]
8
+ synapseforge run [project_dir]
9
+
10
+ Examples:
11
+ synapseforge init # Create project in current directory
12
+ synapseforge init ./mi-proyecto # Create project in ./mi-proyecto
13
+ synapseforge launch -n mi-app # Build distribution zip (current dir)
14
+ synapseforge launch -p ./mi-proyecto -n mi-app # Build from specific project
15
+ synapseforge launch -n mi-app --skip-frontend # Skip frontend build
16
+ synapseforge launch -n mi-app -c # Compile backend to .pyc
17
+ synapseforge colors # Edit colors in current project
18
+ synapseforge colors ./mi-proyecto # Edit colors in specific project
19
+ synapseforge run # Start dev servers (uvicorn + npm)
20
+ synapseforge run ./mi-proyecto # Start dev servers in specific project
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import json
27
+ import re
28
+ import signal
29
+ import subprocess
30
+ import sys
31
+ import time
32
+ import urllib.request
33
+ from pathlib import Path
34
+
35
+
36
+ HEX_RE = re.compile(r"^#[0-9a-fA-F]{6}$")
37
+
38
+ COLOR_FIELDS = [
39
+ ("primary", "Color principal (botones, headers, burbujas)"),
40
+ ("secondary", "Color secundario (hover, detalles light)"),
41
+ ("primary_text", "Color de texto (botones, headers)"),
42
+ ("gradient_secondary", "Color secundario del gradiente"),
43
+ ]
44
+
45
+
46
+ def main() -> None:
47
+ """Parse arguments and dispatch to ``pipeline.init`` or ``pipeline.launch``."""
48
+ parser = argparse.ArgumentParser(
49
+ prog="synapseforge",
50
+ description="synapseForge — AI agent project scaffolding & distribution",
51
+ formatter_class=argparse.RawDescriptionHelpFormatter,
52
+ epilog=__doc__,
53
+ )
54
+ subparsers = parser.add_subparsers(dest="command", required=True, metavar="<command>")
55
+
56
+ # ── init ────────────────────────────────────────────────────────────
57
+ init_p = subparsers.add_parser(
58
+ "init",
59
+ help="Create a new synapseForge project from template",
60
+ description="Create a new synapseForge project from template. Opens a GUI to collect project configuration (name, description, colors, etc.) and generates the complete project structure.",
61
+ formatter_class=argparse.RawDescriptionHelpFormatter,
62
+ epilog="""Examples:
63
+ synapseforge init # Create project in current directory
64
+ synapseforge init ./mi-proyecto # Create project in ./mi-proyecto
65
+ synapseforge init /ruta/absoluta # Create project in absolute path
66
+
67
+ The GUI will prompt for:
68
+ - Project name, description, client/task names
69
+ - Logo files (company + client)
70
+ - Primary, secondary, text color and gradient colors for the UI
71
+
72
+ Output: Complete project structure with backend/, frontend/, config/, pipeline/, .commands/""",
73
+ )
74
+ init_p.add_argument(
75
+ "target_dir",
76
+ nargs="?",
77
+ default=".",
78
+ help="Target directory (default: current working directory)",
79
+ )
80
+
81
+ # ── launch ──────────────────────────────────────────────────────────
82
+ launch_p = subparsers.add_parser(
83
+ "launch",
84
+ help="Build a standalone distribution zip from a project",
85
+ description="Build a standalone distribution zip from a synapseForge project. Packages the backend (with embedded Python), frontend (built), and creates a Windows installer/executable.",
86
+ formatter_class=argparse.RawDescriptionHelpFormatter,
87
+ epilog="""Examples:
88
+ synapseforge launch -n mi-app # Build distribution (current dir)
89
+ synapseforge launch -p ./mi-proyecto -n mi-app # Build from specific project
90
+ synapseforge launch -n mi-app --skip-frontend # Use existing frontend/dist
91
+ synapseforge launch -n mi-app --no-embed # Use system Python (no embed)
92
+
93
+ Arguments:
94
+ -p, --path Path to the project root (default: current directory)
95
+ -n, --name Name for the executable (e.g., mi-app -> mi-app.exe) [required]
96
+
97
+ Options:
98
+ --skip-frontend Skip npm build, use existing frontend/dist/
99
+ --no-embed Use existing venv instead of downloading embedded Python
100
+ -c, --compile Compile backend/ to .pyc (default: ship .py sources)
101
+
102
+ Output: pipeline/dist/<exe_name>.zip containing installer + portable version""",
103
+ )
104
+ launch_p.add_argument(
105
+ "-p",
106
+ "--path",
107
+ default=".",
108
+ help="Path to the project root (default: current working directory)",
109
+ )
110
+ launch_p.add_argument(
111
+ "-n",
112
+ "--name",
113
+ required=True,
114
+ help="Name for the executable (e.g., mi-app)",
115
+ )
116
+ launch_p.add_argument(
117
+ "--skip-frontend",
118
+ action="store_true",
119
+ help="Skip npm build (use existing frontend/dist/)",
120
+ )
121
+ launch_p.add_argument(
122
+ "--no-embed",
123
+ action="store_true",
124
+ help="Use existing venv instead of downloading embedded Python",
125
+ )
126
+ launch_p.add_argument(
127
+ "-c",
128
+ "--compile",
129
+ action="store_true",
130
+ help="Compile backend/ to .pyc (default: ship .py sources as-is)",
131
+ )
132
+
133
+ # ── colors ──────────────────────────────────────────────────────────
134
+ colors_p = subparsers.add_parser(
135
+ "colors",
136
+ help="Edit runtime colors.json (frontend/public/colors.json) via GUI",
137
+ description="Open a tkinter GUI to edit the runtime colors.json file. Changes apply immediately on browser reload — no rebuild needed.",
138
+ formatter_class=argparse.RawDescriptionHelpFormatter,
139
+ epilog="""Examples:
140
+ synapseforge colors # Edit colors in current project
141
+ synapseforge colors ./mi-proyecto # Edit colors in specific project
142
+
143
+ The GUI allows editing:
144
+ - Primary color (buttons, headers, bubbles)
145
+ - Secondary color (hover, details light)
146
+ - Text color for buttons and headers
147
+ - Gradient secondary color (with toggle to enable/disable gradients)
148
+
149
+ Changes are saved to frontend/public/colors.json and take effect on browser refresh.""",
150
+ )
151
+ colors_p.add_argument(
152
+ "project_dir",
153
+ nargs="?",
154
+ default=".",
155
+ help="Project directory containing frontend/public/colors.json (default: current)",
156
+ )
157
+
158
+ # ── run ─────────────────────────────────────────────────────────────
159
+ run_p = subparsers.add_parser(
160
+ "run",
161
+ help="Start development servers (uvicorn + npm run dev) and open browser",
162
+ description="Start both backend (uvicorn with --reload) and frontend (npm run dev) development servers. First executes .commands/init.ps1 to activate the project's venv and load aliases. Opens the default browser to the frontend URL.",
163
+ formatter_class=argparse.RawDescriptionHelpFormatter,
164
+ epilog="""Examples:
165
+ synapseforge run # Start dev servers in current project
166
+ synapseforge run ./mi-proyecto # Start dev servers in specific project
167
+
168
+ What it does:
169
+ 1. Executes .commands/init.ps1 (activates venv, loads aliases from commands.json)
170
+ 2. Starts uvicorn on port 8000 (backend/main.py:app with --reload)
171
+ 3. Starts npm run dev (Vite) on port 5173 (frontend/)
172
+ 4. Waits 3 seconds for servers to start
173
+ 5. Opens default browser to http://localhost:5173
174
+
175
+ Press Ctrl+C to stop both servers gracefully.
176
+
177
+ Requirements:
178
+ - Project created with synapseforge init (has .commands/init.ps1)
179
+ - Node.js + npm installed
180
+ - Frontend dependencies installed (npm install in frontend/)""",
181
+ )
182
+ run_p.add_argument(
183
+ "project_dir",
184
+ nargs="?",
185
+ default=".",
186
+ help="Project root directory (default: current working directory)",
187
+ )
188
+
189
+ args = parser.parse_args()
190
+
191
+ try:
192
+ if args.command == "init":
193
+ _init(args.target_dir)
194
+ elif args.command == "launch":
195
+ _launch(args.path, args.name, args.skip_frontend, args.no_embed, args.compile)
196
+ elif args.command == "colors":
197
+ _colors(args.project_dir)
198
+ elif args.command == "run":
199
+ _run(args.project_dir)
200
+ else:
201
+ parser.print_help()
202
+ sys.exit(1)
203
+ except Exception as exc:
204
+ print(f"ERROR: {exc}", file=sys.stderr)
205
+ sys.exit(1)
206
+
207
+
208
+ def _init(target_dir: str) -> None:
209
+ """Open tkinter GUI to collect config, then run pipeline.init.main.run()."""
210
+ try:
211
+ from synapseforge.tk.init_app import InitApp
212
+ except ImportError as exc:
213
+ print(f"ERROR: could not load GUI module — {exc}", file=sys.stderr)
214
+ sys.exit(1)
215
+
216
+ config = InitApp.launch(target_dir)
217
+ if config is None:
218
+ print(" Inicialización cancelada.")
219
+ return
220
+
221
+ print(" Proyecto creado correctamente.")
222
+
223
+
224
+ def _launch(
225
+ repo_path: str,
226
+ exe_name: str,
227
+ skip_frontend: bool,
228
+ no_embed: bool,
229
+ compile_backend: bool = False,
230
+ ) -> None:
231
+ """Import and run pipeline.launch.forge.build()."""
232
+ try:
233
+ from pipeline.launch.forge import build
234
+ except ImportError as exc:
235
+ print(f"ERROR: could not load launch module — {exc}", file=sys.stderr)
236
+ sys.exit(1)
237
+ build(
238
+ repo_path,
239
+ exe_name,
240
+ skip_frontend=skip_frontend,
241
+ use_embed=not no_embed,
242
+ compile_backend=compile_backend,
243
+ )
244
+
245
+
246
+ def _colors(project_dir: str) -> None:
247
+ """Open tkinter GUI for editing frontend/public/colors.json."""
248
+ try:
249
+ from synapseforge.tk.colors_app import ColorsApp
250
+ except ImportError as exc:
251
+ print(f"ERROR: could not load GUI module — {exc}", file=sys.stderr)
252
+ sys.exit(1)
253
+
254
+ result = ColorsApp.launch(project_dir)
255
+ if result is not None:
256
+ project_path = Path(project_dir).resolve()
257
+ colors_path = project_path / "frontend" / "public" / "colors.json"
258
+ print(f"\n✓ Colores actualizados en {colors_path}")
259
+ print(" Recargá el navegador para ver los cambios (sin rebuild).")
260
+
261
+
262
+ def _wait_for_backend(proc: subprocess.Popen, host: str, port: int, timeout: float = 60.0) -> None:
263
+ """Block until the backend /health endpoint responds or the process dies.
264
+
265
+ Args:
266
+ proc: The uvicorn subprocess (checked for early exit).
267
+ host: Backend host.
268
+ port: Backend port.
269
+ timeout: Maximum seconds to wait.
270
+
271
+ Raises:
272
+ SystemExit: If the backend process exits early or the timeout is reached.
273
+ """
274
+ url = f"http://{host}:{port}/health"
275
+ deadline = time.time() + timeout
276
+ while time.time() < deadline:
277
+ if proc.poll() is not None:
278
+ print(" ERROR: Backend falló al iniciar (¿olvidaste activar el venv?)", file=sys.stderr)
279
+ sys.exit(1)
280
+ try:
281
+ with urllib.request.urlopen(url, timeout=2) as resp:
282
+ if resp.status == 200:
283
+ return
284
+ except Exception:
285
+ pass # Backend still starting — keep waiting
286
+ time.sleep(1)
287
+ print(f" ERROR: El backend no respondió en {url} tras {int(timeout)}s", file=sys.stderr)
288
+ proc.terminate()
289
+ proc.wait()
290
+ sys.exit(1)
291
+
292
+
293
+ def _run(project_dir: str) -> None:
294
+ """Start uvicorn + npm run dev and open the browser."""
295
+ import os
296
+
297
+ # ── Verificar que el entorno virtual esté activado ────────────────
298
+ if not os.environ.get("VIRTUAL_ENV"):
299
+ raise RuntimeError(
300
+ "No hay un entorno virtual activado. "
301
+ "Activá el venv antes de ejecutar 'synapseforge run'.\n"
302
+ "Ejemplo: .\\venv\\Scripts\\activate (Windows) o source venv/bin/activate (Linux/Mac)"
303
+ )
304
+
305
+ project_path = Path(project_dir).resolve()
306
+ frontend_path = project_path / "frontend"
307
+ backend_module = "backend.main:app"
308
+
309
+ if not frontend_path.is_dir():
310
+ print(f"ERROR: No se encontró el directorio frontend en {project_path}", file=sys.stderr)
311
+ sys.exit(1)
312
+
313
+ print(f"Starting dev servers for {project_path} ...")
314
+
315
+ # Start uvicorn (backend) — venv debe estar activo manualmente
316
+ print(" Iniciando backend (uvicorn)...")
317
+ uvicorn_proc = subprocess.Popen(
318
+ ["python", "-m", "uvicorn", backend_module, "--reload", "--port", "8000"],
319
+ cwd=str(project_path),
320
+ shell=True,
321
+ )
322
+
323
+ # Wait for the backend to be fully up before starting the frontend
324
+ print(" Esperando a que el backend esté listo...")
325
+ _wait_for_backend(uvicorn_proc, host="127.0.0.1", port=8000, timeout=60)
326
+
327
+ print(" Backend iniciado correctamente")
328
+
329
+ # Start npm run dev (frontend)
330
+ print(" Iniciando frontend (npm run dev)...")
331
+ npm_proc = subprocess.Popen(
332
+ ["npm", "run", "dev"],
333
+ cwd=str(frontend_path),
334
+ shell=True,
335
+ )
336
+
337
+ time.sleep(3)
338
+ if npm_proc.poll() is not None:
339
+ print(" ERROR: Frontend falló al iniciar", file=sys.stderr)
340
+ uvicorn_proc.terminate()
341
+ uvicorn_proc.wait()
342
+ sys.exit(1)
343
+
344
+ print(" Frontend iniciado correctamente")
345
+
346
+ print("\nPresioná Ctrl+C para detener ambos servidores.\n")
347
+
348
+ def _stop(signum=None, frame=None) -> None:
349
+ """Force-kill both process trees and exit.
350
+
351
+ ``shell=True`` wraps each command in ``cmd.exe``, so ``terminate()``
352
+ alone would leave the real uvicorn/node children alive. We use
353
+ ``taskkill /F /T`` to kill the whole tree. This also force-kills
354
+ uvicorn even when it is stuck in a graceful shutdown waiting for open
355
+ SSE connections.
356
+
357
+ Before force-killing, we give the backend a chance to run its graceful
358
+ shutdown (which frees the local Ollama model) by hitting ``/api/shutdown``.
359
+ """
360
+ print("\nDeteniendo servidores...")
361
+ # Darle chance al backend de liberar el modelo antes de matarlo
362
+ try:
363
+ import urllib.request
364
+
365
+ urllib.request.urlopen("http://127.0.0.1:8000/api/shutdown", timeout=2)
366
+ time.sleep(5)
367
+ except Exception:
368
+ pass
369
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(uvicorn_proc.pid)], capture_output=True)
370
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(npm_proc.pid)], capture_output=True)
371
+ print("Servidores detenidos.")
372
+ sys.exit(0)
373
+
374
+ # Handle Ctrl+C via a signal handler instead of a blocking wait(). A plain
375
+ # ``uvicorn_proc.wait()`` can hang forever because uvicorn's graceful
376
+ # shutdown waits for open SSE connections ("Waiting for connections to
377
+ # close"). Polling with a short sleep keeps the main thread interruptible so
378
+ # the handler runs immediately on Ctrl+C.
379
+ signal.signal(signal.SIGINT, _stop)
380
+ try:
381
+ while True:
382
+ if uvicorn_proc.poll() is not None or npm_proc.poll() is not None:
383
+ break
384
+ time.sleep(0.5)
385
+ except KeyboardInterrupt:
386
+ _stop()
387
+
388
+
389
+ if __name__ == "__main__":
390
+ main()
@@ -0,0 +1 @@
1
+ """GUI modules for synapseForge CLI (tkinter-based)."""