Se agregó soporte para la gestión de proyectos y scripts de Python en la aplicación. Se implementaron nuevas rutas API para crear, obtener, actualizar y eliminar proyectos y scripts de Python. Además, se realizaron ajustes en la interfaz de usuario para incluir un panel de control para scripts de Python, mejorando la experiencia del usuario al interactuar con proyectos de Python. Se actualizaron los directorios de trabajo y se corrigieron rutas en varios archivos de configuración.

This commit is contained in:
Miguel 2025-06-20 20:04:30 +02:00
parent e196dca9c4
commit 13ceda63ba
13 changed files with 55734 additions and 18538 deletions

242
app.py
View File

@ -3,6 +3,7 @@ from flask_sock import Sock
from lib.config_manager import ConfigurationManager
from lib.launcher_manager import LauncherManager
from lib.csharp_launcher_manager import CSharpLauncherManager
from lib.python_launcher_manager import PythonLauncherManager
import os
import json # Added import
from datetime import datetime
@ -31,6 +32,9 @@ launcher_manager = LauncherManager(config_manager.data_path)
# Inicializar C# launcher manager
csharp_launcher_manager = CSharpLauncherManager(config_manager.data_path)
# Inicializar Python launcher manager
python_launcher_manager = PythonLauncherManager(config_manager.data_path)
# Lista global para mantener las conexiones WebSocket activas
websocket_connections = set()
@ -991,6 +995,199 @@ def get_all_csharp_executables(project_id):
# === FIN C# LAUNCHER APIs ===
# === PYTHON LAUNCHER APIs ===
@app.route("/api/python-projects", methods=["GET", "POST"])
def handle_python_projects():
"""Gestionar proyectos Python (GET: obtener, POST: crear)"""
if request.method == "GET":
try:
projects = python_launcher_manager.get_python_projects()
return jsonify(projects)
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # POST
try:
data = request.json
result = python_launcher_manager.add_python_project(data)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-projects/<project_id>", methods=["GET", "PUT", "DELETE"])
def handle_python_project(project_id):
"""Gestionar proyecto Python específico (GET: obtener, PUT: actualizar, DELETE: eliminar)"""
if request.method == "GET":
try:
project = python_launcher_manager.get_python_project(project_id)
if not project:
return jsonify({"error": "Project not found"}), 404
return jsonify(project)
except Exception as e:
return jsonify({"error": str(e)}), 500
elif request.method == "PUT":
try:
data = request.json
result = python_launcher_manager.update_python_project(project_id, data)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # DELETE
try:
result = python_launcher_manager.delete_python_project(project_id)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-scripts/<project_id>")
def get_python_scripts(project_id):
"""Obtener scripts de un proyecto Python"""
try:
scripts = python_launcher_manager.get_project_scripts(project_id)
return jsonify(scripts)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-scripts-all/<project_id>")
def get_all_python_scripts(project_id):
"""Obtener TODOS los scripts de un proyecto Python (incluyendo ocultos) para gestión"""
try:
scripts = python_launcher_manager.get_all_project_scripts(project_id)
return jsonify(scripts)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-script-metadata/<project_id>/<script_name>", methods=["GET", "POST"])
def handle_python_script_metadata(project_id, script_name):
"""Gestionar metadatos de un script Python específico"""
if request.method == "GET":
try:
metadata = python_launcher_manager.get_script_metadata(project_id, script_name)
return jsonify(metadata)
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # POST
try:
data = request.json
result = python_launcher_manager.update_script_metadata(project_id, script_name, data)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/execute-python-script", methods=["POST"])
def execute_python_script():
"""Ejecutar script Python con argumentos opcionales"""
try:
data = request.json
project_id = data["project_id"]
script_name = data["script_name"]
script_args = data.get("args", [])
working_dir = data.get("working_dir", None)
run_in_background = data.get("run_in_background", False) # Para servidores MCP, Flask, etc.
result = python_launcher_manager.execute_python_script(
project_id, script_name, script_args, broadcast_message, working_dir, run_in_background
)
return jsonify(result)
except Exception as e:
error_msg = f"Error ejecutando script Python: {str(e)}"
broadcast_message(error_msg)
return jsonify({"error": error_msg}), 500
@app.route("/api/python-favorites", methods=["GET", "POST"])
def handle_python_favorites():
"""Gestionar favoritos del launcher Python"""
if request.method == "GET":
try:
favorites = python_launcher_manager.get_favorites()
return jsonify({"favorites": favorites})
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # POST
try:
data = request.json
project_id = data["project_id"]
script_name = data["script_name"]
result = python_launcher_manager.toggle_favorite(project_id, script_name)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-history", methods=["GET", "DELETE"])
def handle_python_history():
"""Gestionar historial del launcher Python"""
if request.method == "GET":
try:
history = python_launcher_manager.get_history()
return jsonify({"history": history})
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # DELETE
try:
result = python_launcher_manager.clear_history()
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-categories")
def get_python_categories():
"""Obtener categorías disponibles del launcher Python"""
try:
categories = python_launcher_manager.get_categories()
return jsonify(categories)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-running-processes")
def get_python_running_processes():
"""Obtener procesos Python en ejecución"""
try:
processes = python_launcher_manager.get_running_processes()
return jsonify({"processes": processes})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-process-terminate/<int:pid>", methods=["POST"])
def terminate_python_process(pid):
"""Cerrar un proceso Python"""
try:
result = python_launcher_manager.terminate_process(pid)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-process-focus/<int:pid>", methods=["POST"])
def focus_python_process(pid):
"""Activar foco de un proceso Python"""
try:
result = python_launcher_manager.focus_process(pid)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/python-markdown/<project_id>")
def get_python_markdown_files(project_id):
"""Obtener archivos Markdown de un proyecto Python"""
try:
markdown_files = python_launcher_manager.get_markdown_files(project_id)
return jsonify({"files": markdown_files})
except Exception as e:
print(f"Error getting markdown files for Python project {project_id}: {e}")
# Devolver lista vacía en lugar de error para no interferir con scripts
return jsonify({"files": []})
@app.route("/api/python-markdown-content/<project_id>/<path:relative_path>")
def get_python_markdown_content(project_id, relative_path):
"""Obtener contenido de un archivo Markdown de un proyecto Python"""
try:
result = python_launcher_manager.read_markdown_file(project_id, relative_path)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
# === FIN PYTHON LAUNCHER APIs ===
# --- Helper function to find VS Code ---
def find_vscode_executable():
"""Intenta encontrar el ejecutable de VS Code en ubicaciones comunes y en el PATH."""
@ -1058,10 +1255,23 @@ def open_group_in_editor(editor, group_system, group_id):
"status": "error",
"message": f"Directorio del proyecto C# '{project['name']}' no encontrado"
}), 404
elif group_system == 'python':
project = python_launcher_manager.get_python_project(group_id)
if not project:
return jsonify({
"status": "error",
"message": f"Proyecto Python '{group_id}' no encontrado"
}), 404
script_group_path = project["directory"]
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del proyecto Python '{project['name']}' no encontrado"
}), 404
else:
return jsonify({
"status": "error",
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher' o 'csharp'"
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher', 'csharp' o 'python'"
}), 400
# Definir rutas de ejecutables
@ -1176,10 +1386,23 @@ def open_group_folder(group_system, group_id):
"status": "error",
"message": f"Directorio del proyecto C# '{project['name']}' no encontrado"
}), 404
elif group_system == 'python':
project = python_launcher_manager.get_python_project(group_id)
if not project:
return jsonify({
"status": "error",
"message": f"Proyecto Python '{group_id}' no encontrado"
}), 404
script_group_path = project["directory"]
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del proyecto Python '{project['name']}' no encontrado"
}), 404
else:
return jsonify({
"status": "error",
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher' o 'csharp'"
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher', 'csharp' o 'python'"
}), 400
# Abrir en el explorador según el sistema operativo
@ -1247,10 +1470,23 @@ def get_group_path(group_system, group_id):
"status": "error",
"message": f"Directorio del proyecto C# '{project['name']}' no encontrado"
}), 404
elif group_system == 'python':
project = python_launcher_manager.get_python_project(group_id)
if not project:
return jsonify({
"status": "error",
"message": f"Proyecto Python '{group_id}' no encontrado"
}), 404
script_group_path = project["directory"]
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del proyecto Python '{project['name']}' no encontrado"
}), 404
else:
return jsonify({
"status": "error",
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher' o 'csharp'"
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher', 'csharp' o 'python'"
}), 400
return jsonify({

View File

@ -4,13 +4,7 @@
"path": "."
},
{
"path": "C:/Program Files/Siemens/Automation/Portal V19/PublicAPI/V19/Schemas"
},
{
"path": "../../../../../../Trabajo/VM/44 - 98050 - Fiera/Reporte/ExportsTia/Source/98050_PLC"
},
{
"path": "../../../../../../Trabajo/VM/22 - 93841 - Sidel - Tilting/Reporte/TiaExports"
"path": "C:/Trabajo/SIDEL/13 - E5.007560 - Modifica O&U - SAE235/Reporte/ExportTia"
}
],
"settings": {}

File diff suppressed because it is too large Load Diff

View File

@ -1,90 +1,31 @@
--- Log de Ejecución: x7_clear.py ---
Grupo: XML Parser to SCL
Directorio de Trabajo: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\Source
Inicio: 2025-06-13 01:01:10
Fin: 2025-06-13 01:01:11
Duración: 0:00:00.701052
Directorio de Trabajo: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\ExportTia
Inicio: 2025-06-20 18:53:46
Fin: 2025-06-20 18:53:47
Duración: 0:00:01.131243
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
INFO: format_variable_name importado desde generators.generator_utils
=== Limpiando PLC: 98050_PLC ===
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\MiniMotor\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\MiniMotor\DBS55_PN_Extend-A\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\SICK AG\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\TRANSFER\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\ConveyorsBase\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\Motion\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\Motion\Siemens\LCamHdl_Types\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\Motion\Technology\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\SeamlessDivider\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\SeamlessDivider\Technology\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Machine\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Machine\Cycle\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcTags\parsing
- Eliminado directorio de parsing: 98050_PLC\PlcTags\Library\Motion\Siemens\LCamHdl_Tags\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\1-AIR Philosophy\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\2-TTOP Philosophy\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\3-Motors Manage\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\3-Motors Manage\MiniMotor_PN\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\3-Motors Manage\MiniMotor_PN\MiniMotor_PN\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\HMI\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\MACHINE SIGNALS\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\OB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!!TRANSFER\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\0 - MAIN\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\0 - MAIN\FC\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\0 - MAIN\OB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Device\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Device\FB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Device\FC\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\General\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\General\FC\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Motor\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Motor\DB\Minimotor\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Motor\FC\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Motor\FC\Minimotor\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\4 - LUBE\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\4 - LUBE\FB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\4 - LUBE\FB\OLD\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\2 - MACHINE\DB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\2 - MACHINE\FB\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\! ConveyorsSTD\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\! ConveyorsSTD\Hmi\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\! ConveyorsSTD\System\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\! ConveyorsSTD\TimeZone\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\AAA_Debug\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\AAA_VirtualMaster\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\ExchangeSignals\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\ExchangeSignals\Loop\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\HMI\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Instances\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Generic\Alarms\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Motion\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Motion\Siemens\LCamHdl_Blocks\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Motion\Technology\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Motion\Utilities\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\SeamlessDivider\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\SeamlessDivider\Technology\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Machine\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Machine\Instances\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Setup\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\TimingBelt (downstream divider)\parsing
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\TimingBelt (downstream divider)\Instances\parsing
- Eliminado directorio 'scl_output': 98050_PLC\scl_output
- Eliminado directorio 'xref_output': 98050_PLC\xref_output
- Eliminado archivo agregado: 98050_PLC\full_project_representation.md
- Eliminado log: log_98050_PLC.txt
=== Limpiando PLC: PLC ===
- Eliminado directorio de parsing: PLC\PlcDataTypes\parsing
- Eliminado directorio de parsing: PLC\PlcDataTypes_CR\parsing
- Eliminado directorio de parsing: PLC\PlcTags\parsing
- Eliminado directorio de parsing: PLC\PlcTags\IO Not in Hardware\parsing
- Eliminado directorio de parsing: PLC\ProgramBlocks_CR\parsing
- Eliminado directorio de parsing: PLC\ProgramBlocks_CR\40_10_GNS_PLCdia Main\parsing
- Eliminado directorio de parsing: PLC\ProgramBlocks_XML\parsing
- Eliminado directorio de parsing: PLC\ProgramBlocks_XML\40_10_GNS_PLCdia Main\parsing
- Eliminado directorio de parsing: PLC\SystemBlocks_CR\parsing
- Eliminado directorio 'scl_output': PLC\scl_output
- Eliminado directorio 'xref_output': PLC\xref_output
- Eliminado archivo agregado: PLC\full_project_representation.md
- Eliminado log: log_PLC.txt
--- Resumen de limpieza ---
Directorios eliminados: 70
Directorios eliminados: 11
Archivos eliminados: 2
Limpieza completada.

View File

@ -15,5 +15,5 @@
"xref_source_subdir": "source"
},
"level3": {},
"working_directory": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source"
"working_directory": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia"
}

View File

@ -1,8 +1,8 @@
{
"path": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"path": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
"history": [
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML",
"C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\IOExport"

27144
data/log.txt

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,900 @@
import os
import json
import subprocess
import sys
import threading
import time
from typing import Dict, Any, List, Optional
from datetime import datetime
import uuid
class PythonLauncherManager:
def __init__(self, data_path: str):
self.data_path = data_path
self.launcher_config_path = os.path.join(data_path, "python_launcher_projects.json")
self.favorites_path = os.path.join(data_path, "python_launcher_favorites.json")
self.history_path = os.path.join(data_path, "python_launcher_history.json")
self.script_metadata_path = os.path.join(data_path, "python_launcher_script_metadata.json")
# Procesos en ejecución para Python (servidores, etc.)
self.running_processes = {}
self.process_lock = threading.Lock()
# Inicializar archivos si no existen
self._initialize_files()
def _initialize_files(self):
"""Crear archivos de configuración por defecto si no existen"""
# Inicializar python_launcher_projects.json
if not os.path.exists(self.launcher_config_path):
default_config = {
"version": "1.0",
"projects": [],
"categories": {
"MCP Servers": {
"color": "#3B82F6",
"icon": "🔌",
"subcategories": ["Anthropic", "Custom", "OpenAI"]
},
"Flask Apps": {
"color": "#10B981",
"icon": "🌐",
"subcategories": ["API", "Web App", "Microservice"]
},
"Scripts": {
"color": "#8B5CF6",
"icon": "📜",
"subcategories": ["Automatización", "Utiles", "Procesamiento"]
},
"Bots": {
"color": "#F59E0B",
"icon": "🤖",
"subcategories": ["Discord", "Telegram", "Slack"]
},
"Data Processing": {
"color": "#EF4444",
"icon": "📊",
"subcategories": ["ETL", "Analysis", "ML"]
},
"Otros": {
"color": "#6B7280",
"icon": "📁",
"subcategories": ["Misceláneos"]
}
},
"settings": {
"default_execution_directory": "project_directory",
"enable_argument_validation": True,
"max_history_entries": 100,
"auto_cleanup_days": 30,
"default_python_env": "base"
}
}
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(default_config, f, indent=2, ensure_ascii=False)
# Inicializar python_launcher_favorites.json
if not os.path.exists(self.favorites_path):
default_favorites = {"favorites": []}
with open(self.favorites_path, 'w', encoding='utf-8') as f:
json.dump(default_favorites, f, indent=2, ensure_ascii=False)
# Inicializar python_launcher_history.json
if not os.path.exists(self.history_path):
default_history = {
"history": [],
"settings": {
"max_entries": 100,
"auto_cleanup_days": 30,
"track_execution_time": True,
"track_arguments": True
}
}
with open(self.history_path, 'w', encoding='utf-8') as f:
json.dump(default_history, f, indent=2, ensure_ascii=False)
# Inicializar python_launcher_script_metadata.json
if not os.path.exists(self.script_metadata_path):
default_metadata = {
"version": "1.0",
"script_metadata": {}
}
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
json.dump(default_metadata, f, indent=2, ensure_ascii=False)
def get_python_projects(self) -> List[Dict[str, Any]]:
"""Obtener todos los proyectos Python"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
return config.get("projects", [])
except Exception as e:
print(f"Error loading Python projects: {e}")
return []
def get_python_project(self, project_id: str) -> Optional[Dict[str, Any]]:
"""Obtener un proyecto específico por ID"""
projects = self.get_python_projects()
for project in projects:
if project.get("id") == project_id:
return project
return None
def add_python_project(self, project_data: Dict[str, Any]) -> Dict[str, str]:
"""Agregar nuevo proyecto Python"""
try:
# Validar datos requeridos
required_fields = ["name", "directory"]
for field in required_fields:
if not project_data.get(field):
return {"status": "error", "message": f"Campo requerido: {field}"}
# Validar que el directorio existe
if not os.path.isdir(project_data["directory"]):
return {"status": "error", "message": "El directorio especificado no existe"}
# Generar ID único si no se proporciona
if not project_data.get("id"):
project_data["id"] = str(uuid.uuid4())[:8]
# Verificar que el ID no exista
if self.get_python_project(project_data["id"]):
return {"status": "error", "message": "Ya existe un proyecto con este ID"}
# Agregar campos por defecto
current_time = datetime.now().isoformat() + "Z"
project_data.setdefault("description", "")
project_data.setdefault("category", "Otros")
project_data.setdefault("version", "1.0")
project_data.setdefault("author", "")
project_data.setdefault("tags", [])
project_data.setdefault("python_env", "base") # Entorno Python por defecto
project_data.setdefault("created_date", current_time)
project_data["updated_date"] = current_time
# Cargar configuración y agregar proyecto
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
config["projects"].append(project_data)
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
return {"status": "success", "message": "Proyecto agregado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error agregando proyecto: {str(e)}"}
def update_python_project(self, project_id: str, project_data: Dict[str, Any]) -> Dict[str, str]:
"""Actualizar proyecto existente"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# Buscar y actualizar el proyecto
project_found = False
for i, project in enumerate(config["projects"]):
if project["id"] == project_id:
# Mantener ID y fechas de creación
project_data["id"] = project_id
project_data["created_date"] = project.get("created_date", datetime.now().isoformat() + "Z")
project_data["updated_date"] = datetime.now().isoformat() + "Z"
config["projects"][i] = project_data
project_found = True
break
if not project_found:
return {"status": "error", "message": "Proyecto no encontrado"}
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
return {"status": "success", "message": "Proyecto actualizado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error actualizando proyecto: {str(e)}"}
def delete_python_project(self, project_id: str) -> Dict[str, str]:
"""Eliminar proyecto Python"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# Filtrar el proyecto a eliminar
original_count = len(config["projects"])
config["projects"] = [p for p in config["projects"] if p["id"] != project_id]
if len(config["projects"]) == original_count:
return {"status": "error", "message": "Proyecto no encontrado"}
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
# Limpiar metadatos y favoritos relacionados
self._cleanup_script_metadata_for_project(project_id)
self._cleanup_favorites_for_project(project_id)
return {"status": "success", "message": "Proyecto eliminado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error eliminando proyecto: {str(e)}"}
def get_project_scripts(self, project_id: str) -> List[Dict[str, Any]]:
"""Obtener scripts de un proyecto (solo .py visibles)"""
project = self.get_python_project(project_id)
if not project:
return []
project_dir = project["directory"]
if not os.path.isdir(project_dir):
return []
scripts = []
script_metadata = self._load_script_metadata()
# Buscar archivos .py en el directorio del proyecto
for filename in os.listdir(project_dir):
if filename.endswith('.py') and not filename.startswith('__'):
script_path = os.path.join(project_dir, filename)
if os.path.isfile(script_path):
# Obtener metadatos del script
metadata_key = f"{project_id}:{filename}"
metadata = script_metadata.get("script_metadata", {}).get(metadata_key, {})
# Solo mostrar scripts no ocultos
if not metadata.get("hidden", False):
scripts.append({
"filename": filename,
"display_name": metadata.get("display_name", filename.replace('.py', '')),
"description": metadata.get("description", ""),
"tags": metadata.get("tags", []),
"arguments": metadata.get("arguments", []),
"is_server": metadata.get("is_server", False), # Indica si es un servidor que corre en background
"server_port": metadata.get("server_port", ""),
"requires_background": metadata.get("requires_background", False)
})
return sorted(scripts, key=lambda x: x["display_name"])
def get_all_project_scripts(self, project_id: str) -> List[Dict[str, Any]]:
"""Obtener TODOS los scripts de un proyecto (incluyendo ocultos) para gestión"""
project = self.get_python_project(project_id)
if not project:
return []
project_dir = project["directory"]
if not os.path.isdir(project_dir):
return []
scripts = []
script_metadata = self._load_script_metadata()
# Buscar archivos .py en el directorio del proyecto
for filename in os.listdir(project_dir):
if filename.endswith('.py') and not filename.startswith('__'):
script_path = os.path.join(project_dir, filename)
if os.path.isfile(script_path):
# Obtener metadatos del script
metadata_key = f"{project_id}:{filename}"
metadata = script_metadata.get("script_metadata", {}).get(metadata_key, {})
scripts.append({
"filename": filename,
"display_name": metadata.get("display_name", filename.replace('.py', '')),
"description": metadata.get("description", ""),
"tags": metadata.get("tags", []),
"arguments": metadata.get("arguments", []),
"hidden": metadata.get("hidden", False),
"is_server": metadata.get("is_server", False),
"server_port": metadata.get("server_port", ""),
"requires_background": metadata.get("requires_background", False)
})
return sorted(scripts, key=lambda x: x["display_name"])
def get_script_metadata(self, project_id: str, script_name: str) -> Dict[str, Any]:
"""Obtener metadatos de un script específico"""
script_metadata = self._load_script_metadata()
metadata_key = f"{project_id}:{script_name}"
return script_metadata.get("script_metadata", {}).get(metadata_key, {})
def update_script_metadata(self, project_id: str, script_name: str, metadata: Dict[str, Any]) -> Dict[str, str]:
"""Actualizar metadatos de un script"""
try:
script_metadata = self._load_script_metadata()
metadata_key = f"{project_id}:{script_name}"
if "script_metadata" not in script_metadata:
script_metadata["script_metadata"] = {}
script_metadata["script_metadata"][metadata_key] = metadata
self._save_script_metadata(script_metadata)
return {"status": "success", "message": "Metadatos actualizados exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error actualizando metadatos: {str(e)}"}
def get_available_python_envs(self) -> List[Dict[str, str]]:
"""Obtener lista de entornos de Python/Miniconda disponibles"""
try:
envs = [{"name": "base", "display_name": "Base (Sistema)", "path": sys.executable}]
# Intentar encontrar Miniconda
miniconda_paths = [
r"C:\Users\migue\miniconda3",
r"C:\ProgramData\miniconda3",
r"C:\miniconda3",
os.path.expanduser("~/miniconda3"),
os.path.expanduser("~/anaconda3")
]
for base_path in miniconda_paths:
if os.path.exists(base_path):
envs_path = os.path.join(base_path, "envs")
if os.path.exists(envs_path):
for env_name in os.listdir(envs_path):
env_path = os.path.join(envs_path, env_name)
python_exe = os.path.join(env_path, "python.exe")
if os.path.exists(python_exe):
envs.append({
"name": env_name,
"display_name": f"{env_name} (Miniconda)",
"path": python_exe
})
break # Solo usar el primer Miniconda encontrado
return envs
except Exception as e:
print(f"Error getting Python environments: {e}")
return [{"name": "base", "display_name": "Base (Sistema)", "path": sys.executable}]
def execute_python_script(self, project_id: str, script_name: str, script_args: List[str],
broadcast_func, working_dir: str = None, run_in_background: bool = False) -> Dict[str, Any]:
"""Ejecutar script Python con argumentos opcionales"""
try:
project = self.get_python_project(project_id)
if not project:
return {"error": "Proyecto no encontrado"}
# Construir ruta del script
script_path = os.path.join(project["directory"], script_name)
if not os.path.exists(script_path):
return {"error": f"Script '{script_name}' no encontrado"}
# Determinar directorio de trabajo
if working_dir and os.path.isdir(working_dir):
work_dir = working_dir
else:
work_dir = project["directory"]
# Obtener ejecutable de Python
python_env = project.get("python_env", "base")
python_exe = self._get_python_executable(python_env)
# Construir comando
cmd = [python_exe, script_path] + script_args
# ID único para esta ejecución
execution_id = str(uuid.uuid4())[:8]
start_time = time.time()
broadcast_func(f"🚀 Ejecutando script: {script_name}")
broadcast_func(f"📁 Directorio: {work_dir}")
broadcast_func(f"🐍 Python: {python_exe}")
if script_args:
broadcast_func(f"⚙️ Argumentos: {' '.join(script_args)}")
# Agregar a historial
history_entry = {
"id": execution_id,
"project_id": project_id,
"script_name": script_name,
"arguments": script_args,
"working_directory": work_dir,
"python_env": python_env,
"timestamp": datetime.now().isoformat() + "Z",
"status": "running",
"execution_time": None
}
self._add_to_history(history_entry)
# Configurar proceso
if sys.platform == "win32":
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
if run_in_background:
# Para procesos en background (servidores), crear ventana nueva
creationflags |= subprocess.CREATE_NEW_CONSOLE
else:
creationflags = 0
# Ejecutar proceso
process = subprocess.Popen(
cmd,
cwd=work_dir,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
universal_newlines=True,
creationflags=creationflags if sys.platform == "win32" else None
)
# Guardar proceso en la lista de procesos activos
with self.process_lock:
self.running_processes[process.pid] = {
"pid": process.pid,
"project_id": project_id,
"script_name": script_name,
"start_time": datetime.now().isoformat() + "Z",
"execution_id": execution_id,
"working_directory": work_dir,
"is_background": run_in_background
}
broadcast_func(f"✅ Proceso iniciado con PID: {process.pid}")
if run_in_background:
# Para procesos en background, no esperamos la salida
broadcast_func(f"🔄 Script ejecutándose en segundo plano (PID: {process.pid})")
return {
"status": "success",
"message": f"Script '{script_name}' iniciado en segundo plano",
"execution_id": execution_id,
"pid": process.pid,
"background": True
}
else:
# Para scripts normales, leer salida en tiempo real
def read_output():
try:
for line in iter(process.stdout.readline, ''):
if line:
broadcast_func(line.rstrip())
except Exception as e:
broadcast_func(f"Error leyendo salida: {e}")
finally:
if process.stdout:
process.stdout.close()
# Iniciar lectura de salida en hilo separado
output_thread = threading.Thread(target=read_output, daemon=True)
output_thread.start()
# Monitorear finalización del proceso
def monitor_completion():
try:
return_code = process.wait()
end_time = time.time()
execution_time = end_time - start_time
# Actualizar historial
self._update_history_status(execution_id, return_code, execution_time)
# Remover de procesos activos
with self.process_lock:
if process.pid in self.running_processes:
del self.running_processes[process.pid]
if return_code == 0:
broadcast_func(f"✅ Script completado exitosamente (código: {return_code})")
else:
broadcast_func(f"❌ Script terminó con errores (código: {return_code})")
broadcast_func(f"⏱️ Tiempo de ejecución: {execution_time:.2f} segundos")
except Exception as e:
broadcast_func(f"Error monitoreando proceso: {e}")
# Iniciar monitoreo en hilo separado
monitor_thread = threading.Thread(target=monitor_completion, daemon=True)
monitor_thread.start()
return {
"status": "success",
"message": f"Script '{script_name}' ejecutándose...",
"execution_id": execution_id,
"pid": process.pid,
"background": False
}
except Exception as e:
error_msg = f"Error ejecutando script Python: {str(e)}"
broadcast_func(error_msg)
return {"error": error_msg}
def _get_python_executable(self, env_name: str) -> str:
"""Obtener ejecutable de Python para el entorno especificado"""
if env_name == "base":
return sys.executable
# Intentar encontrar entorno de conda en todas las ubicaciones posibles
miniconda_paths = [
r"C:\Users\migue\miniconda3",
r"C:\ProgramData\miniconda3",
r"C:\miniconda3",
os.path.expanduser("~/miniconda3"),
os.path.expanduser("~/anaconda3")
]
for base_path in miniconda_paths:
if os.path.exists(base_path):
env_path = os.path.join(base_path, "envs", env_name)
python_exe = os.path.join(env_path, "python.exe")
if os.path.exists(python_exe):
return python_exe
# Fallback al Python del sistema
print(f"Warning: Python environment '{env_name}' not found, using system Python")
return sys.executable
def _load_script_metadata(self) -> Dict[str, Any]:
"""Cargar metadatos de scripts desde archivo"""
try:
with open(self.script_metadata_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
return {"version": "1.0", "script_metadata": {}}
def _save_script_metadata(self, metadata: Dict[str, Any]):
"""Guardar metadatos de scripts en archivo"""
try:
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
json.dump(metadata, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving script metadata: {e}")
def _cleanup_script_metadata_for_project(self, project_id: str):
"""Limpiar metadatos de scripts al eliminar un proyecto"""
try:
script_metadata = self._load_script_metadata()
if "script_metadata" in script_metadata:
# Filtrar metadatos que no pertenezcan al proyecto eliminado
script_metadata["script_metadata"] = {
k: v for k, v in script_metadata["script_metadata"].items()
if not k.startswith(f"{project_id}:")
}
self._save_script_metadata(script_metadata)
except Exception as e:
print(f"Error cleaning script metadata for project {project_id}: {e}")
def get_favorites(self) -> List[Dict[str, Any]]:
"""Obtener scripts favoritos"""
try:
with open(self.favorites_path, 'r', encoding='utf-8') as f:
favorites_data = json.load(f)
return favorites_data.get("favorites", [])
except Exception:
return []
def toggle_favorite(self, project_id: str, script_name: str) -> Dict[str, str]:
"""Agregar o quitar de favoritos"""
try:
favorites_data = {"favorites": self.get_favorites()}
# Buscar si ya está en favoritos
favorite_key = f"{project_id}:{script_name}"
existing_favorite = None
for i, fav in enumerate(favorites_data["favorites"]):
if fav.get("project_id") == project_id and fav.get("script_name") == script_name:
existing_favorite = i
break
if existing_favorite is not None:
# Quitar de favoritos
del favorites_data["favorites"][existing_favorite]
message = "Removido de favoritos"
is_favorite = False
else:
# Agregar a favoritos
project = self.get_python_project(project_id)
if project:
script_metadata = self.get_script_metadata(project_id, script_name)
favorites_data["favorites"].append({
"project_id": project_id,
"project_name": project["name"],
"script_name": script_name,
"display_name": script_metadata.get("display_name", script_name.replace('.py', '')),
"description": script_metadata.get("description", ""),
"added_date": datetime.now().isoformat() + "Z"
})
message = "Agregado a favoritos"
is_favorite = True
else:
return {"status": "error", "message": "Proyecto no encontrado"}
with open(self.favorites_path, 'w', encoding='utf-8') as f:
json.dump(favorites_data, f, indent=2, ensure_ascii=False)
return {"status": "success", "message": message, "is_favorite": is_favorite}
except Exception as e:
return {"status": "error", "message": f"Error gestionando favoritos: {str(e)}"}
def get_history(self) -> List[Dict[str, Any]]:
"""Obtener historial de ejecuciones"""
try:
with open(self.history_path, 'r', encoding='utf-8') as f:
history_data = json.load(f)
return history_data.get("history", [])
except Exception:
return []
def clear_history(self) -> Dict[str, str]:
"""Limpiar historial de ejecuciones"""
try:
history_data = {
"history": [],
"settings": {
"max_entries": 100,
"auto_cleanup_days": 30,
"track_execution_time": True,
"track_arguments": True
}
}
with open(self.history_path, 'w', encoding='utf-8') as f:
json.dump(history_data, f, indent=2, ensure_ascii=False)
return {"status": "success", "message": "Historial limpiado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error limpiando historial: {str(e)}"}
def get_categories(self) -> Dict[str, Any]:
"""Obtener categorías disponibles"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
return config.get("categories", {})
except Exception:
return {}
def _add_to_history(self, entry: Dict[str, Any]):
"""Agregar entrada al historial"""
try:
history_data = {"history": self.get_history()}
# Agregar nueva entrada al inicio
history_data["history"].insert(0, entry)
# Mantener máximo de entradas
max_entries = 100
if len(history_data["history"]) > max_entries:
history_data["history"] = history_data["history"][:max_entries]
with open(self.history_path, 'w', encoding='utf-8') as f:
json.dump(history_data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error adding to history: {e}")
def _cleanup_favorites_for_project(self, project_id: str):
"""Limpiar favoritos al eliminar un proyecto"""
try:
favorites_data = {"favorites": self.get_favorites()}
# Filtrar favoritos que no pertenezcan al proyecto eliminado
favorites_data["favorites"] = [
fav for fav in favorites_data["favorites"]
if fav.get("project_id") != project_id
]
with open(self.favorites_path, 'w', encoding='utf-8') as f:
json.dump(favorites_data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error cleaning favorites for project {project_id}: {e}")
def _update_history_status(self, execution_id: str, final_code: int, final_execution_time: float):
"""Actualizar estado final en el historial"""
try:
history_data = {"history": self.get_history()}
for entry in history_data["history"]:
if entry.get("id") == execution_id:
entry["status"] = "completed" if final_code == 0 else "error"
entry["return_code"] = final_code
entry["execution_time"] = final_execution_time
break
with open(self.history_path, 'w', encoding='utf-8') as f:
json.dump(history_data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error updating history status: {e}")
def focus_process(self, pid: int) -> Dict[str, str]:
"""Intentar dar foco a un proceso por su PID (Windows)"""
try:
if sys.platform == "win32":
import ctypes
from ctypes import wintypes
def enum_windows_proc(hwnd, pid):
if ctypes.windll.user32.IsWindowVisible(hwnd):
process_id = wintypes.DWORD()
ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(process_id))
if process_id.value == pid:
ctypes.windll.user32.SetForegroundWindow(hwnd)
return False # Detener enumeración
return True # Continuar enumeración
# Definir el tipo de callback
EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)
callback = EnumWindowsProc(enum_windows_proc)
ctypes.windll.user32.EnumWindows(callback, pid)
return {"status": "success", "message": f"Intentando dar foco al proceso {pid}"}
else:
return {"status": "info", "message": "Función de foco no disponible en esta plataforma"}
except Exception as e:
return {"status": "error", "message": f"Error dando foco al proceso: {str(e)}"}
def terminate_process(self, pid: int) -> Dict[str, str]:
"""Terminar un proceso por su PID"""
try:
with self.process_lock:
if pid in self.running_processes:
process_info = self.running_processes[pid]
del self.running_processes[pid]
# Intentar terminar el proceso
if sys.platform == "win32":
subprocess.run(["taskkill", "/F", "/PID", str(pid)],
capture_output=True, check=False)
else:
import signal
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
pass # Proceso ya terminado
return {
"status": "success",
"message": f"Proceso {pid} terminado ({process_info.get('script_name', 'N/A')})"
}
else:
return {"status": "error", "message": "Proceso no encontrado en la lista de procesos activos"}
except Exception as e:
return {"status": "error", "message": f"Error terminando proceso: {str(e)}"}
def get_running_processes(self) -> List[Dict[str, Any]]:
"""Obtener lista de procesos en ejecución"""
try:
with self.process_lock:
processes = []
dead_pids = []
for pid, info in self.running_processes.items():
# Verificar si el proceso sigue vivo
try:
if sys.platform == "win32":
result = subprocess.run(
["tasklist", "/FI", f"PID eq {pid}"],
capture_output=True, text=True, check=False
)
if str(pid) not in result.stdout:
dead_pids.append(pid)
continue
else:
os.kill(pid, 0) # No mata el proceso, solo verifica si existe
except (ProcessLookupError, subprocess.SubprocessError):
dead_pids.append(pid)
continue
# Agregar información del proceso
project = self.get_python_project(info["project_id"])
processes.append({
"pid": pid,
"project_id": info["project_id"],
"project_name": project["name"] if project else "Proyecto no encontrado",
"script_name": info["script_name"],
"start_time": info["start_time"],
"execution_id": info["execution_id"],
"working_directory": info["working_directory"],
"is_background": info.get("is_background", False)
})
# Limpiar procesos muertos
for pid in dead_pids:
del self.running_processes[pid]
return processes
except Exception as e:
print(f"Error getting running processes: {e}")
return []
def get_markdown_files(self, project_id: str) -> List[Dict[str, Any]]:
"""Obtener archivos Markdown de un proyecto"""
try:
project = self.get_python_project(project_id)
if not project:
return []
project_dir = project["directory"]
if not os.path.isdir(project_dir):
return []
markdown_files = []
# Buscar archivos .md en el directorio del proyecto
for root, dirs, files in os.walk(project_dir):
# Excluir directorios comunes que no contienen documentación relevante
dirs[:] = [d for d in dirs if d not in ['.git', '__pycache__', '.vscode', 'node_modules']]
for filename in files:
if filename.lower().endswith('.md'):
file_path = os.path.join(root, filename)
relative_path = os.path.relpath(file_path, project_dir)
# Obtener información básica del archivo
try:
stat = os.stat(file_path)
size = stat.st_size
modified = datetime.fromtimestamp(stat.st_mtime).isoformat() + "Z"
# Intentar leer las primeras líneas para obtener el título
title = filename.replace('.md', '')
try:
with open(file_path, 'r', encoding='utf-8') as f:
first_line = f.readline().strip()
if first_line.startswith('#'):
title = first_line.lstrip('#').strip()
except Exception:
pass
markdown_files.append({
"filename": filename,
"relative_path": relative_path.replace('\\', '/'), # Normalizar separadores
"title": title,
"size": size,
"modified": modified
})
except Exception as e:
print(f"Error getting file info for {file_path}: {e}")
continue
# Ordenar por ruta relativa
return sorted(markdown_files, key=lambda x: x["relative_path"])
except Exception as e:
print(f"Error getting markdown files for project {project_id}: {e}")
return []
def read_markdown_file(self, project_id: str, relative_path: str) -> Dict[str, Any]:
"""Obtener contenido de un archivo Markdown"""
try:
project = self.get_python_project(project_id)
if not project:
return {"error": "Proyecto no encontrado"}
# Construir ruta completa y validar que esté dentro del proyecto
project_dir = os.path.abspath(project["directory"])
file_path = os.path.abspath(os.path.join(project_dir, relative_path))
# Verificar que el archivo esté dentro del directorio del proyecto (seguridad)
if not file_path.startswith(project_dir):
return {"error": "Acceso no autorizado al archivo"}
if not os.path.exists(file_path):
return {"error": "Archivo no encontrado"}
# Leer contenido del archivo
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Obtener información del archivo
stat = os.stat(file_path)
return {
"content": content,
"filename": os.path.basename(file_path),
"relative_path": relative_path,
"size": stat.st_size,
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat() + "Z"
}
except Exception as e:
return {"error": f"Error leyendo archivo: {str(e)}"}

View File

@ -620,7 +620,6 @@ class LauncherManager {
// === GESTIÓN DE GRUPOS (actualizada) ===
populateGroupForm(group) {
document.getElementById('group-id').value = group.id;
document.getElementById('group-name').value = group.name;
document.getElementById('group-description').value = group.description || '';
document.getElementById('group-category').value = group.category;
@ -630,7 +629,6 @@ class LauncherManager {
}
clearGroupForm() {
document.getElementById('group-id').value = '';
document.getElementById('group-name').value = '';
document.getElementById('group-description').value = '';
document.getElementById('group-category').value = 'Otros';
@ -642,7 +640,6 @@ class LauncherManager {
async saveGroup() {
const formData = {
id: document.getElementById('group-id').value,
name: document.getElementById('group-name').value,
description: document.getElementById('group-description').value,
category: document.getElementById('group-category').value,
@ -1380,6 +1377,15 @@ function switchTab(tabName) {
window.csharpLauncherManager.init();
}
}
// Inicializar Python launcher si es la primera vez
if (tabName === 'python') {
if (typeof initPythonLauncher === 'function') {
initPythonLauncher();
} else {
console.error('initPythonLauncher function not found! Make sure python_launcher.js is loaded.');
}
}
}
// Funciones para modales

1251
static/js/python_launcher.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -128,6 +128,17 @@
Launcher C#
</span>
</button>
<button id="python-tab" onclick="switchTab('python')"
class="tab-button py-2 px-1 border-b-2 font-medium text-sm">
<span class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M7 16h.01">
</path>
</svg>
Python Scripts
</span>
</button>
</nav>
</div>
</div>
@ -498,6 +509,155 @@
</div>
</div>
<!-- Tab Content: Python Scripts -->
<div id="python-content" class="tab-content hidden">
<!-- Python Project Controls -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Python Scripts - MCP Servers & Background Scripts</h2>
<button onclick="openPythonProjectEditor()"
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
onmousedown="if(!window.openPythonProjectEditor) alert('Python Launcher cargando...')">
Gestionar Proyectos
</button>
</div>
<!-- Project Selector -->
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Seleccionar Proyecto Python</label>
<div class="flex gap-2">
<div class="relative flex-1">
<select id="python-project-select" class="w-full p-3 border rounded-lg pl-12"
onchange="loadPythonScripts()">
<option value="">-- Seleccionar Proyecto --</option>
</select>
<div class="absolute left-3 top-1/2 transform -translate-y-1/2">
<div id="selected-python-project-icon"
class="w-6 h-6 bg-gray-200 rounded flex items-center justify-center text-sm">🐍
</div>
</div>
</div>
<button onclick="openPythonProjectInEditor('vscode')"
class="bg-blue-500 text-white px-4 py-3 rounded-lg hover:bg-blue-600" id="vscode-python-btn"
style="display: none;" title="Abrir proyecto en VS Code">
<img src="{{ url_for('static', filename='icons/vscode.png') }}" class="w-5 h-5"
alt="VS Code Icon">
</button>
<button onclick="openPythonProjectInEditor('cursor')"
class="bg-purple-500 text-white px-4 py-3 rounded-lg hover:bg-purple-600"
id="cursor-python-btn" style="display: none;" title="Abrir proyecto en Cursor">
<img src="{{ url_for('static', filename='icons/cursor.png') }}" class="w-5 h-5"
alt="Cursor Icon">
</button>
<button onclick="openPythonProjectFolder()"
class="bg-green-500 text-white px-4 py-3 rounded-lg hover:bg-green-600"
id="folder-python-btn" style="display: none;" title="Abrir carpeta del proyecto">
📁
</button>
<button onclick="copyPythonProjectPath()"
class="bg-gray-500 text-white px-4 py-3 rounded-lg hover:bg-gray-600"
id="copy-path-python-btn" style="display: none;" title="Copiar path del proyecto">
📋
</button>
</div>
</div>
<!-- Category Filter -->
<div class="mb-4">
<h3 class="text-sm font-medium mb-2">Filtrar por Categoría</h3>
<div class="flex flex-wrap gap-2">
<button class="python-category-btn active px-3 py-1 rounded-full text-sm border"
data-category="all" onclick="filterPythonByCategory('all')">
Todas
</button>
<button class="python-category-btn px-3 py-1 rounded-full text-sm border"
data-category="MCP Servers" onclick="filterPythonByCategory('MCP Servers')">
🔌 MCP Servers
</button>
<button class="python-category-btn px-3 py-1 rounded-full text-sm border"
data-category="Flask Apps" onclick="filterPythonByCategory('Flask Apps')">
🌐 Flask Apps
</button>
<button class="python-category-btn px-3 py-1 rounded-full text-sm border"
data-category="Scripts" onclick="filterPythonByCategory('Scripts')">
📜 Scripts
</button>
<button class="python-category-btn px-3 py-1 rounded-full text-sm border" data-category="Bots"
onclick="filterPythonByCategory('Bots')">
🤖 Bots
</button>
</div>
</div>
</div>
<!-- Python Favorites Panel -->
<div id="python-favorites-panel" class="mb-6 bg-green-50 border border-green-200 rounded-lg p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold text-green-800">
⭐ Scripts Favoritos
</h3>
<span class="text-sm text-green-600" id="python-favorites-count">
0 favoritos
</span>
</div>
<div id="python-favorites-list" class="space-y-2">
<!-- Lista dinámica de favoritos Python -->
</div>
</div>
<!-- Python Scripts Grid -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Scripts Disponibles</h2>
<button onclick="openPythonScriptManager()"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
id="manage-python-scripts-btn" style="display: none;">
Gestionar Scripts
</button>
</div>
<div id="python-scripts-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Script cards dinámicos -->
</div>
</div>
<!-- Python Markdown Files Section -->
<div id="python-markdown-files-section" class="mb-6 bg-white p-6 rounded-lg shadow" style="display: none;">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">📄 Documentación (Markdown)</h2>
<span class="text-sm text-gray-500">Archivos .md en el directorio del proyecto</span>
</div>
<div id="python-markdown-files-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
<!-- Markdown files cards dinámicos -->
</div>
</div>
<!-- Running Processes Panel -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">🔄 Procesos en Ejecución</h3>
<button onclick="refreshPythonProcesses()" class="text-blue-500 hover:text-blue-700 text-sm">
Actualizar
</button>
</div>
<div id="python-running-processes" class="space-y-2">
<!-- Lista dinámica de procesos -->
</div>
</div>
<!-- History Panel -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">📋 Historial de Ejecuciones</h3>
<button onclick="clearPythonHistory()" class="text-red-500 hover:text-red-700 text-sm">
Limpiar Historial
</button>
</div>
<div id="python-history-list" class="space-y-2 max-h-64 overflow-y-auto">
<!-- Lista dinámica de historial -->
</div>
</div>
</div>
<!-- Modal: Editor de Proyectos C# -->
<div id="csharp-project-editor-modal"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
@ -749,16 +909,11 @@
<!-- Formulario de edición -->
<form id="group-form" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium mb-1">ID del Grupo</label>
<input type="text" id="group-id" class="w-full p-2 border rounded" required>
</div>
<!-- El ID se genera automáticamente basado en el nombre -->
<div>
<label class="block text-sm font-medium mb-1">Nombre</label>
<input type="text" id="group-name" class="w-full p-2 border rounded" required>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Descripción</label>
@ -1063,10 +1218,301 @@
</div>
</div>
<!-- Python Project Editor Modal -->
<div id="python-project-editor-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-screen overflow-y-auto">
<div class="p-6">
<h3 class="text-xl font-semibold mb-6">Gestionar Proyectos Python</h3>
<!-- Lista de proyectos existentes -->
<div class="mb-6">
<h4 class="font-medium mb-3">Proyectos Existentes</h4>
<div id="existing-python-projects-list"
class="space-y-2 max-h-40 overflow-y-auto border rounded p-2">
<!-- Lista dinámica -->
</div>
</div>
<!-- Formulario de edición -->
<form id="python-project-form" class="space-y-4"
onsubmit="event.preventDefault(); savePythonProject();">
<!-- El ID se genera automáticamente -->
<div>
<label class="block text-sm font-medium mb-1">Nombre</label>
<input type="text" id="python-project-name" class="w-full p-2 border rounded" required>
</div>
<div>
<label class="block text-sm font-medium mb-1">Descripción</label>
<textarea id="python-project-description" class="w-full p-2 border rounded h-20"></textarea>
</div>
<div class="grid grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium mb-1">Categoría</label>
<select id="python-project-category" class="w-full p-2 border rounded">
<option value="MCP Servers">🔗 MCP Servers</option>
<option value="Flask Apps">🌐 Flask Apps</option>
<option value="Scripts">📝 Scripts</option>
<option value="Bots">🤖 Bots</option>
<option value="Data Processing">📊 Data Processing</option>
<option value="Otros">📁 Otros</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">Versión</label>
<input type="text" id="python-project-version" class="w-full p-2 border rounded"
value="1.0">
</div>
<div>
<label class="block text-sm font-medium mb-1">Entorno Python</label>
<select id="python-project-python-env" class="w-full p-2 border rounded">
<option value="base">base</option>
</select>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Directorio</label>
<div class="flex gap-2">
<input type="text" id="python-project-directory" class="flex-1 p-2 border rounded"
required>
<button type="button" onclick="browsePythonProjectDirectory()"
class="bg-gray-500 text-white px-4 py-2 rounded">
Explorar
</button>
</div>
</div>
<div class="flex justify-end gap-3 pt-4">
<button type="button" onclick="closePythonProjectEditor()"
class="px-4 py-2 text-gray-600 hover:text-gray-800">
Cancelar
</button>
<button type="button" onclick="deletePythonProject()"
class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
id="delete-python-project-btn" style="display: none;">
Eliminar
</button>
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Guardar
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Python Script Manager Modal -->
<div id="python-script-manager-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-screen overflow-y-auto">
<div class="p-6">
<h3 class="text-xl font-semibold mb-6">Gestionar Scripts del Proyecto</h3>
<p class="text-gray-600 mb-4" id="python-script-manager-project-info">Selecciona un proyecto para
gestionar sus scripts</p>
<!-- Lista de scripts -->
<div class="space-y-3" id="python-script-manager-list">
<!-- Lista dinámica de scripts -->
</div>
<div class="flex justify-end gap-3 pt-4 mt-6 border-t">
<button type="button" onclick="closePythonScriptManager()"
class="px-4 py-2 text-gray-600 hover:text-gray-800">
Cerrar
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Python Script Metadata Editor Modal -->
<div id="python-script-metadata-editor-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-lg w-full max-h-screen overflow-y-auto">
<div class="p-6">
<h3 class="text-lg font-semibold mb-4">Editar Metadatos del Script Python</h3>
<form id="python-script-metadata-form" class="space-y-4"
onsubmit="event.preventDefault(); savePythonScriptMetadata();">
<input type="hidden" id="edit-python-meta-project-id">
<input type="hidden" id="edit-python-meta-script-name">
<div>
<label class="block text-sm font-bold mb-1">Nombre del Archivo</label>
<p id="edit-python-meta-filename-display"
class="text-sm text-gray-600 bg-gray-100 p-2 rounded border"></p>
</div>
<div>
<label for="edit-python-meta-display-name" class="block text-sm font-bold mb-2">Nombre a
Mostrar</label>
<input type="text" id="edit-python-meta-display-name" class="w-full p-2 border rounded"
required>
</div>
<div>
<label for="edit-python-meta-description" class="block text-sm font-bold mb-2">Descripción
Corta</label>
<input type="text" id="edit-python-meta-description" class="w-full p-2 border rounded">
</div>
<div>
<label for="edit-python-meta-long-description"
class="block text-sm font-bold mb-2">Descripción Larga / Ayuda</label>
<textarea id="edit-python-meta-long-description" class="w-full p-2 border rounded"
rows="5"></textarea>
<p class="text-xs text-gray-500 mt-1">Usa Markdown. Doble Enter para párrafo nuevo, dos
espacios + Enter para salto de línea simple.</p>
</div>
<div class="flex items-center">
<input type="checkbox" id="edit-python-meta-hidden" class="form-checkbox h-5 w-5 mr-2">
<label for="edit-python-meta-hidden" class="text-sm font-bold">Ocultar script (no aparecerá
en la lista de ejecución)</label>
</div>
<div class="flex justify-end gap-3 pt-4">
<button type="button" onclick="closePythonScriptMetadataEditor()"
class="px-4 py-2 text-gray-600 hover:text-gray-800">
Cancelar
</button>
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Guardar Cambios
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Python Script Description Modal -->
<div id="python-script-description-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[80vh] overflow-hidden">
<div class="p-6 border-b">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-semibold" id="python-desc-modal-script-name">Descripción del Script
</h3>
<p class="text-sm text-gray-600" id="python-desc-modal-script-file"></p>
</div>
<button onclick="closePythonScriptDescription()"
class="text-gray-500 hover:text-gray-700 text-2xl">&times;</button>
</div>
</div>
<div class="p-6 overflow-y-auto max-h-[60vh]">
<div id="python-script-description-content" class="prose prose-sm max-w-none">
<!-- Contenido markdown renderizado -->
</div>
</div>
<div class="p-4 border-t bg-gray-50 flex justify-end">
<button onclick="closePythonScriptDescription()"
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600">
Cerrar
</button>
</div>
</div>
</div>
</div>
<!-- Python Markdown Viewer Modal -->
<div id="python-markdown-viewer-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden">
<div class="p-6 border-b">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-semibold" id="python-markdown-viewer-title">Documento Markdown</h3>
<p class="text-sm text-gray-600" id="python-markdown-viewer-path"></p>
</div>
<button onclick="closePythonMarkdownViewer()"
class="text-gray-500 hover:text-gray-700 text-2xl">&times;</button>
</div>
</div>
<div class="p-6 overflow-y-auto max-h-[75vh]">
<div id="python-markdown-viewer-content" class="prose prose-lg max-w-none">
<!-- Contenido markdown renderizado -->
</div>
</div>
<div class="p-4 border-t bg-gray-50 flex justify-end">
<button onclick="closePythonMarkdownViewer()"
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600">
Cerrar
</button>
</div>
</div>
</div>
</div>
<!-- Python Script Options Modal -->
<div id="python-script-options-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
<div class="p-6">
<h3 class="text-lg font-semibold mb-4">Opciones de Ejecución - Python</h3>
<div id="python-script-info" class="mb-4 p-3 bg-gray-50 rounded">
<div class="font-medium" id="python-script-display-name"></div>
<div class="text-sm text-gray-600" id="python-script-description"></div>
</div>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium mb-1">
Argumentos de Línea de Comandos
</label>
<textarea id="python-script-args-input" class="w-full p-2 border rounded h-20"
placeholder="--port 8080 --debug"></textarea>
<p class="text-xs text-gray-500 mt-1">
Separar argumentos con espacios. Usar comillas para valores con espacios.
</p>
</div>
<div>
<label class="block text-sm font-medium mb-1">
Tipo de Ejecución
</label>
<div class="flex gap-2">
<label class="flex items-center">
<input type="radio" name="python-execution-type" value="false" checked class="mr-2">
<span class="text-sm">🖥️ Normal</span>
</label>
<label class="flex items-center">
<input type="radio" name="python-execution-type" value="true" class="mr-2">
<span class="text-sm">🚀 Background</span>
</label>
</div>
<p class="text-xs text-gray-500 mt-1">
Normal: Ejecuta y muestra salida. Background: Para servidores MCP, Flask apps, etc.
</p>
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
<button onclick="closePythonScriptOptions()"
class="px-4 py-2 text-gray-600 hover:text-gray-800">
Cancelar
</button>
<button onclick="executePythonScriptWithOptions()"
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Ejecutar Script
</button>
</div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
<script src="{{ url_for('static', filename='js/scripts.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/launcher.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/csharp_launcher.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/python_launcher.js') }}" defer></script>
<script>
// Inicializar markdown-it globalmente
window.markdownit = window.markdownit || markdownit;