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,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
|
synapseforge/__init__.py
ADDED
synapseforge/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package — init and launch subcommands."""
|
synapseforge/cli/main.py
ADDED
|
@@ -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)."""
|