stackhelx 1.0.0__tar.gz → 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stackhelx-1.0.0 → stackhelx-1.0.1}/CHANGELOG.md +10 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/PKG-INFO +1 -1
- {stackhelx-1.0.0 → stackhelx-1.0.1}/docs/plan-expansion.md +29 -29
- {stackhelx-1.0.0 → stackhelx-1.0.1}/docs/plan-lenguajes.md +3 -3
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stack.example.yaml +3 -3
- stackhelx-1.0.1/stackhelx/__init__.py +2 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/ports.py +4 -4
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/registry.py +2 -2
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/runner.py +6 -4
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/scripts.py +1 -1
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/server.py +5 -5
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_cli.py +3 -3
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_detect.py +2 -2
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_mcp.py +3 -3
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_runner.py +41 -3
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_server.py +4 -4
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_tunnel.py +4 -4
- stackhelx-1.0.0/stackhelx/__init__.py +0 -2
- {stackhelx-1.0.0 → stackhelx-1.0.1}/.github/workflows/ci.yml +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/.github/workflows/release.yml +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/.gitignore +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/AGENTS.md +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/CLAUDE.md +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/LICENSE +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/README.md +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/docs/comandos.md +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/docs/deteccion.md +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/docs/interfaz.md +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/docs/pendientes.md +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/docs/plan-siguiente.md +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/docs/stack-yaml.md +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/docs/superpowers/plans/2026-08-08-partir-readme.md +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/docs/superpowers/plans/2026-08-08-release-1-0-1.md +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/docs/superpowers/plans/2026-08-18-campo-url.md +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/docs/superpowers/specs/2026-07-29-autodeteccion-design.md +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/pyproject.toml +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/scripts/check_docs.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/scripts/check_metadata.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/__main__.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/browse.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/cli.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/config.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/detect.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/docker.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/doctor.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/guardrails.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/history.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/mcp.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/tunnel.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/web/app.css +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/web/app.js +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/web/index.html +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/stackhelx/web/tokens.css +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/conftest.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_browse.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_config.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_conftest.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_guardrails.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_history.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_ports.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_registry.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/tests/test_scripts.py +0 -0
- {stackhelx-1.0.0 → stackhelx-1.0.1}/uv.lock +0 -0
|
@@ -4,6 +4,16 @@ Formato de [Keep a Changelog](https://keepachangelog.com/es/1.1.0/).
|
|
|
4
4
|
Versionado semántico: la superficie pública son los comandos del CLI, el
|
|
5
5
|
esquema de `stack.yaml` y las rutas de la API local.
|
|
6
6
|
|
|
7
|
+
## [1.0.1] - 2026-09-13
|
|
8
|
+
|
|
9
|
+
### Seguridad y Limpieza
|
|
10
|
+
|
|
11
|
+
- **Purga de mensajes de seguridad y excepciones (`ports.py`):** Los mensajes de `KillRefused` ante intento de cerrar el PID propio o el de la terminal padre ahora identifican explícitamente a `StackHelx`.
|
|
12
|
+
- **Precedencia de entorno global (`runner.py`):** `build_env()` lee `~/.stackhelx/env.global` y mantiene fallback transparente a `~/.portmaster/env.global` si existe, con pruebas de precedencia añadidas a la suite.
|
|
13
|
+
- **Títulos y logs de servidor (`server.py`):** Título de FastAPI fijado a `StackHelx`, logger migrado a `stackhelx.server`, y mensajes de guarda en `/api/share` actualizados para rechazar túneles contra la consola de `StackHelx`.
|
|
14
|
+
- **Plantilla de configuración (`stack.example.yaml`):** Actualizada para documentar `stackhelx init` y la nueva identidad de StackHelx.
|
|
15
|
+
- **Suite de pruebas:** 474 pruebas en verde (+1 test de precedencia de entorno global).
|
|
16
|
+
|
|
7
17
|
## [1.0.0] - 2026-09-13
|
|
8
18
|
|
|
9
19
|
### Rebrand y Nuevo Comienzo
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: stackhelx
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Orquestador de entornos de desarrollo locales: puertos, Docker, backend y frontend con un solo comando
|
|
5
5
|
Project-URL: Homepage, https://github.com/TicoraX/StackHelx
|
|
6
6
|
Project-URL: Repository, https://github.com/TicoraX/StackHelx
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# Plan de Expansión Estratégica:
|
|
1
|
+
# Plan de Expansión Estratégica: StackHelx (Ecosistema Completo de Desarrollo)
|
|
2
2
|
|
|
3
3
|
Fecha: 14 de agosto de 2026.
|
|
4
4
|
Rama: `feature/expansion-plan`
|
|
5
5
|
|
|
6
|
-
Este documento define la evolución de
|
|
6
|
+
Este documento define la evolución de StackHelx desde un orquestador de *stacks* locales hacia una **plataforma integral de productividad, diagnóstico, automatización y control del entorno de desarrollo local**.
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -11,17 +11,17 @@ Este documento define la evolución de PortMaster desde un orquestador de *stack
|
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
14
|
-
│
|
|
14
|
+
│ STACKHELX ECOSYSTEM │
|
|
15
15
|
├───────────────────────────────┬───────────────────────────────┬─────────────┤
|
|
16
16
|
│ 1. ORQUESTACIÓN DE STACK │ 2. RUNNER DE TAREAS │ 3. TÚNELES │
|
|
17
17
|
│ - Topological sort & health │ - Scripts y pipelines locales │ - ngrok │
|
|
18
18
|
│ - env_file & pre_start hooks │ - Inyección de variables │ - cloudflare│
|
|
19
|
-
│ - Monorrepos (pnpm/turbo/uv) │ -
|
|
19
|
+
│ - Monorrepos (pnpm/turbo/uv) │ - stackhelx run <tarea> │ - share │
|
|
20
20
|
├───────────────────────────────┼───────────────────────────────┼─────────────┤
|
|
21
21
|
│ 4. MULTI-PROYECTO COMPUESTO │ 5. HIGIENE Y MANTENIMIENTO │ 6. MCP & AI │
|
|
22
22
|
│ - Inclusión inter-repositorios│ - Docker prune inteligente │ - Tools MCP │
|
|
23
23
|
│ - Grupos de servicios │ - Detección de zombies │ - AI Agent │
|
|
24
|
-
│ - Matriz de colisiones │ -
|
|
24
|
+
│ - Matriz de colisiones │ - stackhelx clean │ Diagnosis │
|
|
25
25
|
└───────────────────────────────┴───────────────────────────────┴─────────────┘
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -30,15 +30,15 @@ Este documento define la evolución de PortMaster desde un orquestador de *stack
|
|
|
30
30
|
## 2. Especificación de Fases y Módulos
|
|
31
31
|
|
|
32
32
|
### Fase 1: Variables de Entorno y Hooks del Stack
|
|
33
|
-
* **Módulo**: [`
|
|
33
|
+
* **Módulo**: [`stackhelx/config.py`](../stackhelx/config.py), [`stackhelx/runner.py`](../stackhelx/runner.py)
|
|
34
34
|
* **`env_file`**: Carga de `.env`, `.env.local` o lista ordenada sin dependencias externas.
|
|
35
35
|
* **Hooks de Ciclo de Vida**:
|
|
36
36
|
* `pre_start`: Ejecución síncrona preparatoria (ej. migraciones, build).
|
|
37
37
|
* `post_start`: Ejecución tras confirmación de salud (ej. `seed`, notificación).
|
|
38
|
-
* **Bóveda Global (`~/.
|
|
38
|
+
* **Bóveda Global (`~/.stackhelx/env.global`)**: Herencia automática de variables comunes entre proyectos.
|
|
39
39
|
|
|
40
|
-
### Fase 2: Runner de Tareas y Scripts de Proyecto (`
|
|
41
|
-
* **Módulo**: [`
|
|
40
|
+
### Fase 2: Runner de Tareas y Scripts de Proyecto (`stackhelx run`)
|
|
41
|
+
* **Módulo**: [`stackhelx/scripts.py`](../stackhelx/scripts.py), [`stackhelx/cli.py`](../stackhelx/cli.py)
|
|
42
42
|
* **Declaración en `stack.yaml`**:
|
|
43
43
|
```yaml
|
|
44
44
|
scripts:
|
|
@@ -47,15 +47,15 @@ Este documento define la evolución de PortMaster desde un orquestador de *stack
|
|
|
47
47
|
migrate: alembic upgrade head
|
|
48
48
|
check: [lint, test] # Pipeline secuencial
|
|
49
49
|
```
|
|
50
|
-
* **Comando CLI**: `
|
|
50
|
+
* **Comando CLI**: `stackhelx run <script>` ejecuta en la raíz del proyecto, con el contexto de variables del stack.
|
|
51
51
|
|
|
52
|
-
### Fase 3: Exposición Segura y Compartir (`
|
|
53
|
-
* **Módulo**: [`
|
|
52
|
+
### Fase 3: Exposición Segura y Compartir (`stackhelx share`)
|
|
53
|
+
* **Módulo**: [`stackhelx/tunnel.py`](../stackhelx/tunnel.py), [`stackhelx/cli.py`](../stackhelx/cli.py)
|
|
54
54
|
* **Integración de Túneles**: Detección y manejo de binarios locales (`cloudflared`, `ngrok`, `tailscale`).
|
|
55
|
-
* **Comando CLI & Web**: `
|
|
55
|
+
* **Comando CLI & Web**: `stackhelx share web` genera URL pública temporal, QR en consola y botón directo en la UI web.
|
|
56
56
|
|
|
57
57
|
### Fase 4: Orquestación Multi-Proyecto y Dependencias Inter-Repositorios
|
|
58
|
-
* **Módulo**: [`
|
|
58
|
+
* **Módulo**: [`stackhelx/registry.py`](../stackhelx/registry.py), [`stackhelx/runner.py`](../stackhelx/runner.py)
|
|
59
59
|
* **Proyectos Compuestos (`includes`)**:
|
|
60
60
|
```yaml
|
|
61
61
|
# frontend/stack.yaml
|
|
@@ -63,28 +63,28 @@ Este documento define la evolución de PortMaster desde un orquestador de *stack
|
|
|
63
63
|
includes:
|
|
64
64
|
- ../backend-api # Levanta backend automáticamente si no está corriendo
|
|
65
65
|
```
|
|
66
|
-
* **Grupos de Proyectos**: `
|
|
66
|
+
* **Grupos de Proyectos**: `stackhelx group up <nombre_grupo>` y matriz preventiva de colisión de puertos.
|
|
67
67
|
|
|
68
|
-
### Fase 5: Higiene del Sistema y Docker (`
|
|
69
|
-
* **Módulo**: [`
|
|
68
|
+
### Fase 5: Higiene del Sistema y Docker (`stackhelx clean`)
|
|
69
|
+
* **Módulo**: [`stackhelx/docker.py`](../stackhelx/docker.py), [`stackhelx/cli.py`](../stackhelx/cli.py)
|
|
70
70
|
* **Limpieza Asistida**:
|
|
71
|
-
* `
|
|
72
|
-
* `
|
|
71
|
+
* `stackhelx clean` (con `--volumes` opcional): Remueve contenedores parados, imágenes huérfanas, build-cache y redes no usadas.
|
|
72
|
+
* `stackhelx doctor`: Diagnóstico integral del entorno local y detección de problemas.
|
|
73
73
|
|
|
74
74
|
### Fase 6: Servidor MCP e Integración con Agentes de IA
|
|
75
|
-
* **Módulo**: [`
|
|
75
|
+
* **Módulo**: [`stackhelx/mcp.py`](../stackhelx/mcp.py)
|
|
76
76
|
* **Herramientas MCP Expuestas**:
|
|
77
|
-
* `
|
|
78
|
-
* `
|
|
79
|
-
* `
|
|
80
|
-
* `
|
|
81
|
-
* `
|
|
82
|
-
* `
|
|
83
|
-
* `
|
|
84
|
-
* `
|
|
77
|
+
* `stackhelx_status`: Estado de salud, puertos y procesos.
|
|
78
|
+
* `stackhelx_restart`: Reinicio atómico de un servicio específico.
|
|
79
|
+
* `stackhelx_logs`: Consulta de logs con filtrado para depuración autónoma.
|
|
80
|
+
* `stackhelx_free_port`: Liberación de puertos conflictivos.
|
|
81
|
+
* `stackhelx_clean`: Limpieza segura de recursos Docker.
|
|
82
|
+
* `stackhelx_doctor`: Diagnóstico integral del sistema.
|
|
83
|
+
* `stackhelx_history`: Historial de arranques de proyectos.
|
|
84
|
+
* `stackhelx_init`: Inicialización asistida de `stack.yaml`.
|
|
85
85
|
|
|
86
86
|
### Fase 7: Detección Avanzada (Monorrepos, `uv`, Frameworks) y UI Web Polish
|
|
87
|
-
* **Módulo**: [`
|
|
87
|
+
* **Módulo**: [`stackhelx/detect.py`](../stackhelx/detect.py), [`stackhelx/web/`](../stackhelx/web)
|
|
88
88
|
* Detección de `pnpm-workspace.yaml`, `turbo.json`, `uv.lock`, Astro (`4321`), Vite (`5173`).
|
|
89
89
|
* UI Web: Pausa y búsqueda en streaming de logs, editor de `stack.yaml` y disparador de tareas `run`.
|
|
90
90
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
Fecha: 7 de septiembre de 2026. Rama `main`, 432 tests en verde.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
StackHelx detecta hoy compose, Python, Node, Deno, Go, Rust, Ruby, PHP y .NET.
|
|
19
19
|
Una carpeta con un proyecto de otro lenguaje no devuelve nada y obliga a escribir
|
|
20
20
|
el `stack.yaml` a mano. Este plan cierra los tres huecos que tienen usuarios
|
|
21
21
|
reales de desarrollo local multi-servicio: la JVM (Java y Kotlin), Elixir y Bun
|
|
@@ -298,8 +298,8 @@ primero). Y se verifica revirtiendo, como todo lo demás acá.
|
|
|
298
298
|
|
|
299
299
|
Cada PR toca:
|
|
300
300
|
|
|
301
|
-
- `
|
|
302
|
-
- `
|
|
301
|
+
- `stackhelx/detect.py`: las dos funciones, sus constantes, y el nombre en la tupla de `detect()`.
|
|
302
|
+
- `stackhelx/browse.py`: el marcador nuevo en `MARKERS` (`pom.xml`, `build.gradle`, `mix.exs`, `bunfig.toml`), que es lo que pinta el badge en el explorador de carpetas.
|
|
303
303
|
- `tests/test_detect.py`: los casos de arriba, negativo incluido.
|
|
304
304
|
- `docs/deteccion.md`, `README.md` y `CHANGELOG.md`. De `CLAUDE.md`: lo que se escribe en la doc se comprueba corriendo el comando, no recordándolo.
|
|
305
305
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Esquema de configuracion de
|
|
1
|
+
# Esquema de configuracion de StackHelx.
|
|
2
2
|
#
|
|
3
|
-
# El archivo es opcional: sin uno,
|
|
4
|
-
# `
|
|
3
|
+
# El archivo es opcional: sin uno, StackHelx detecta los servicios del proyecto.
|
|
4
|
+
# `stackhelx init` (o `shx init`) escribe lo detectado aca para editarlo a mano.
|
|
5
5
|
|
|
6
6
|
name: mi-proyecto
|
|
7
7
|
|
|
@@ -39,7 +39,7 @@ TERMINATE_TIMEOUT = 5
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
class KillRefused(Exception):
|
|
42
|
-
"""El proceso existe pero
|
|
42
|
+
"""El proceso existe pero StackHelx se niega a matarlo."""
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
@dataclass(frozen=True)
|
|
@@ -356,7 +356,7 @@ def kill(
|
|
|
356
356
|
no existe, y psutil.AccessDenied si faltan permisos.
|
|
357
357
|
"""
|
|
358
358
|
# psutil.Process(None) es el proceso actual: sin esto, un scan que no vio al
|
|
359
|
-
# dueno del puerto termina en
|
|
359
|
+
# dueno del puerto termina en StackHelx matandose a si mismo.
|
|
360
360
|
if pid is None:
|
|
361
361
|
raise KillRefused("no hay PID que cerrar")
|
|
362
362
|
if pid in PROTECTED_PIDS:
|
|
@@ -364,9 +364,9 @@ def kill(
|
|
|
364
364
|
|
|
365
365
|
me = psutil.Process()
|
|
366
366
|
if pid == me.pid:
|
|
367
|
-
raise KillRefused("ese PID es
|
|
367
|
+
raise KillRefused("ese PID es StackHelx")
|
|
368
368
|
if pid in {ancestor.pid for ancestor in me.parents()}:
|
|
369
|
-
raise KillRefused(f"PID {pid} es un proceso padre de
|
|
369
|
+
raise KillRefused(f"PID {pid} es un proceso padre de StackHelx (tu terminal)")
|
|
370
370
|
|
|
371
371
|
proc = psutil.Process(pid)
|
|
372
372
|
if create_time is not None and proc.create_time() != create_time:
|
|
@@ -116,7 +116,7 @@ _docker_cache: tuple[float, bool] | None = None
|
|
|
116
116
|
def declared_ports(max_age: float = 0.0) -> dict[int, list[Path]]:
|
|
117
117
|
"""Puerto declarado -> proyectos registrados que lo piden.
|
|
118
118
|
|
|
119
|
-
Es el unico dato que
|
|
119
|
+
Es el unico dato que StackHelx tiene y las herramientas de un proyecto solo
|
|
120
120
|
no pueden tener: cada compose se conoce a si mismo y ninguno sabe del de al
|
|
121
121
|
lado. Sin esto, que dos proyectos peleen por el 3000 se descubre cuando el
|
|
122
122
|
segundo no arranca.
|
|
@@ -251,7 +251,7 @@ def _save(items: list[Path]) -> None:
|
|
|
251
251
|
def token() -> str:
|
|
252
252
|
"""Token de la API local.
|
|
253
253
|
|
|
254
|
-
Prioriza PORTMASTER_TOKEN. Si no esta, usa uno generado en el directorio del
|
|
254
|
+
Prioriza STACKHELX_TOKEN (con fallback a PORTMASTER_TOKEN). Si no esta, usa uno generado en el directorio del
|
|
255
255
|
usuario. Ver la desviacion documentada en CLAUDE.md: una herramienta que se
|
|
256
256
|
instala con pipx no puede traer un .env, y un token generado con permisos
|
|
257
257
|
0600 fuera del repo es mas seguro que uno que el usuario copia a mano.
|
|
@@ -32,13 +32,15 @@ from .config import Service, Stack
|
|
|
32
32
|
def build_env(service: Service, extra_env: dict[str, str] | None = None) -> dict[str, str]:
|
|
33
33
|
"""Construye el entorno de ejecucion con precedencia clara:
|
|
34
34
|
1. os.environ
|
|
35
|
-
2. ~/.portmaster/env.global
|
|
35
|
+
2. ~/.stackhelx/env.global (o legacy ~/.portmaster/env.global si existe)
|
|
36
36
|
3. service.env_file (en orden)
|
|
37
37
|
4. service.env (declarado explicito)
|
|
38
38
|
5. extra_env (e.g. desde --env-file en CLI)
|
|
39
39
|
"""
|
|
40
40
|
env = dict(os.environ)
|
|
41
|
-
global_env = Path.home() / ".
|
|
41
|
+
global_env = Path.home() / ".stackhelx" / "env.global"
|
|
42
|
+
if not global_env.is_file():
|
|
43
|
+
global_env = Path.home() / ".portmaster" / "env.global"
|
|
42
44
|
if global_env.is_file():
|
|
43
45
|
env.update(config.parse_env_file(global_env))
|
|
44
46
|
for env_path in service.env_file:
|
|
@@ -692,7 +694,7 @@ def dependency_graph(stack: Stack, profile: str | None = None) -> dict:
|
|
|
692
694
|
def run_stop(service: Service, extra_env: dict[str, str] | None = None) -> subprocess.CompletedProcess | None:
|
|
693
695
|
"""Corre el `stop:` de un servicio. None si no termino a tiempo.
|
|
694
696
|
|
|
695
|
-
Vive afuera del `Runner` porque `
|
|
697
|
+
Vive afuera del `Runner` porque `stackhelx down` tiene que poder apagar un
|
|
696
698
|
stack que arranco otro proceso: los contenedores de un `docker compose up -d`
|
|
697
699
|
sobreviven a la terminal que los levanto, y ahi no hay ningun `Proc` vivo del
|
|
698
700
|
que colgarse.
|
|
@@ -755,7 +757,7 @@ def _port_hint(proc: Proc) -> str:
|
|
|
755
757
|
quien = dueno.name or f"pid {dueno.pid}"
|
|
756
758
|
return (
|
|
757
759
|
f"; el puerto {declarado} ya lo tenia {quien} (pid {dueno.pid}):"
|
|
758
|
-
f" liberalo con `
|
|
760
|
+
f" liberalo con `stackhelx free {declarado}` y reintenta"
|
|
759
761
|
)
|
|
760
762
|
return ""
|
|
761
763
|
|
|
@@ -33,7 +33,7 @@ def _entrecomillar(args: Sequence[str]) -> str:
|
|
|
33
33
|
|
|
34
34
|
Iban con un `" ".join`, y estos comandos corren con `shell=True`: un
|
|
35
35
|
argumento con un separador ejecutaba lo que viniera despues. Desde el CLI lo
|
|
36
|
-
escribe el usuario, pero `portmaster_run` los recibe de un agente de IA.
|
|
36
|
+
escribe el usuario, pero `stackhelx_run` (o `portmaster_run`) los recibe de un agente de IA.
|
|
37
37
|
|
|
38
38
|
Por plataforma, porque el shell no es el mismo. `shlex.join` entrecomilla al
|
|
39
39
|
estilo POSIX y `cmd.exe` no entiende las comillas simples: un `&` adentro le
|
|
@@ -54,7 +54,7 @@ from . import (
|
|
|
54
54
|
# `browse_module` porque el endpoint de /api/browse ya se llama browse.
|
|
55
55
|
from . import browse as browse_module
|
|
56
56
|
|
|
57
|
-
log = logging.getLogger("
|
|
57
|
+
log = logging.getLogger("stackhelx.server")
|
|
58
58
|
|
|
59
59
|
WEB = Path(__file__).parent / "web"
|
|
60
60
|
LOG_LINES = 500
|
|
@@ -265,7 +265,7 @@ class Session:
|
|
|
265
265
|
self.stopped_by_user = False
|
|
266
266
|
self.state = "starting"
|
|
267
267
|
self.sink.write(
|
|
268
|
-
f"\n[
|
|
268
|
+
f"\n[stackhelx] Conmutando al perfil '{new_profile or 'default'}'...\n"
|
|
269
269
|
)
|
|
270
270
|
console = Console(
|
|
271
271
|
file=self.sink, force_terminal=False, no_color=True, width=160, soft_wrap=True
|
|
@@ -707,7 +707,7 @@ def create_app(token: str | None = None) -> FastAPI:
|
|
|
707
707
|
_load_sessions_state()
|
|
708
708
|
token = token or registry.token()
|
|
709
709
|
app = FastAPI(
|
|
710
|
-
title="
|
|
710
|
+
title="StackHelx",
|
|
711
711
|
docs_url=None,
|
|
712
712
|
redoc_url=None,
|
|
713
713
|
openapi_url=None,
|
|
@@ -1520,7 +1520,7 @@ def create_app(token: str | None = None) -> FastAPI:
|
|
|
1520
1520
|
log.info("puerto rechazado: %s", exc)
|
|
1521
1521
|
raise HTTPException(400, str(exc))
|
|
1522
1522
|
|
|
1523
|
-
#
|
|
1523
|
+
# StackHelx no se publica a si mismo. Detras de este puerto esta la API
|
|
1524
1524
|
# que arranca stack.yaml, o sea ejecucion de comandos: exponerla deja al
|
|
1525
1525
|
# token como unica puerta contra internet entero. La validacion de Host
|
|
1526
1526
|
# del middleware ya rechaza al cliente de tuneles, asi que hoy el efecto
|
|
@@ -1534,7 +1534,7 @@ def create_app(token: str | None = None) -> FastAPI:
|
|
|
1534
1534
|
if propio is not None and port == propio:
|
|
1535
1535
|
raise HTTPException(
|
|
1536
1536
|
400,
|
|
1537
|
-
f"el puerto {port} es el de
|
|
1537
|
+
f"el puerto {port} es el de StackHelx. Publicarlo expone la API "
|
|
1538
1538
|
"que ejecuta los comandos de tu stack.yaml, no tu proyecto.",
|
|
1539
1539
|
)
|
|
1540
1540
|
|
|
@@ -29,7 +29,7 @@ def sin_color(texto: str) -> str:
|
|
|
29
29
|
Rich pinta los numeros, asi que `f"El puerto {port} ya esta ocupado"` llega
|
|
30
30
|
partido por un `\x1b[1;36m` en el medio y un `in` plano no lo encuentra. Solo
|
|
31
31
|
aparece cuando el entorno trae `FORCE_COLOR` (la terminal del desarrollador,
|
|
32
|
-
o un `
|
|
32
|
+
o un `stackhelx up` que se lo pone a sus hijos): sin eso Rich ve que no hay
|
|
33
33
|
tty y no pinta nada, que es por lo que en CI el test daba verde.
|
|
34
34
|
|
|
35
35
|
Afirmar sobre el texto pelado, y no sobre como se ve, es lo correcto igual:
|
|
@@ -245,7 +245,7 @@ def _proyecto_con_puerto(tmp_path, nombre, port):
|
|
|
245
245
|
|
|
246
246
|
|
|
247
247
|
def test_doctor_avisa_si_otro_proyecto_declara_el_mismo_puerto(tmp_path, monkeypatch, free_ports):
|
|
248
|
-
"""El dato que solo
|
|
248
|
+
"""El dato que solo StackHelx tiene: ningun compose sabe del de al lado."""
|
|
249
249
|
(port,) = free_ports(1)
|
|
250
250
|
_proyecto_con_puerto(tmp_path, "blog", port)
|
|
251
251
|
fitness = _proyecto_con_puerto(tmp_path, "fitness", port)
|
|
@@ -819,7 +819,7 @@ def test_open_sin_puerto_no_tapa_al_servicio_que_si_contesta(
|
|
|
819
819
|
def test_share_rechaza_un_puerto_fuera_de_rango(tmp_path, monkeypatch):
|
|
820
820
|
"""`target` es texto porque tambien acepta el nombre de un servicio, asi que
|
|
821
821
|
se pierde el min/max que traen los demas comandos. Sin la validacion a mano,
|
|
822
|
-
`
|
|
822
|
+
`stackhelx share 0` levantaba el cliente de tuneles contra 127.0.0.1:0.
|
|
823
823
|
"""
|
|
824
824
|
monkeypatch.chdir(tmp_path)
|
|
825
825
|
for target in ("0", "70000", "99999"):
|
|
@@ -471,7 +471,7 @@ COMPOSE_CON_PERFILES = """
|
|
|
471
471
|
|
|
472
472
|
|
|
473
473
|
def test_un_contenedor_con_perfil_no_arranca_por_defecto(tmp_path):
|
|
474
|
-
"""En compose, `profiles:` excluye. En
|
|
474
|
+
"""En compose, `profiles:` excluye. En StackHelx un perfil es una lista de
|
|
475
475
|
lo que se arranca. Traducirlos al reves arranca lo que compose apaga."""
|
|
476
476
|
write(tmp_path, "compose.yaml", COMPOSE_CON_PERFILES)
|
|
477
477
|
stack = detect.detect(tmp_path)
|
|
@@ -541,7 +541,7 @@ def test_un_compose_sin_perfiles_arranca_todo(tmp_path):
|
|
|
541
541
|
|
|
542
542
|
|
|
543
543
|
def test_congelar_un_compose_con_perfiles_no_cambia_lo_que_arranca(tmp_path):
|
|
544
|
-
"""`
|
|
544
|
+
"""`stackhelx init` tiene que ser fiel: el archivo congelado arranca lo
|
|
545
545
|
mismo que la deteccion, ni un contenedor mas."""
|
|
546
546
|
write(tmp_path, "compose.yaml", COMPOSE_CON_PERFILES)
|
|
547
547
|
detectado = detect.detect(tmp_path)
|
|
@@ -318,7 +318,7 @@ def test_mcp_una_peticion_con_id_si_se_contesta():
|
|
|
318
318
|
|
|
319
319
|
|
|
320
320
|
def test_mcp_tool_call_doctor(tmp_path):
|
|
321
|
-
"""Prueba que
|
|
321
|
+
"""Prueba que stackhelx_doctor ejecuta los checks sin crash."""
|
|
322
322
|
req = {
|
|
323
323
|
"jsonrpc": "2.0",
|
|
324
324
|
"id": 30,
|
|
@@ -332,7 +332,7 @@ def test_mcp_tool_call_doctor(tmp_path):
|
|
|
332
332
|
|
|
333
333
|
|
|
334
334
|
def test_mcp_tool_call_history(tmp_path, monkeypatch):
|
|
335
|
-
"""Prueba que
|
|
335
|
+
"""Prueba que stackhelx_history lee la telemetria."""
|
|
336
336
|
monkeypatch.setattr(mcp.registry, "HOME", tmp_path)
|
|
337
337
|
pid = mcp.registry.project_id(tmp_path)
|
|
338
338
|
mcp.history.append(pid, {"event": "test", "duration_s": 1.5})
|
|
@@ -352,7 +352,7 @@ def test_mcp_tool_call_history(tmp_path, monkeypatch):
|
|
|
352
352
|
|
|
353
353
|
|
|
354
354
|
def test_mcp_tool_call_ports(free_ports):
|
|
355
|
-
"""Prueba que
|
|
355
|
+
"""Prueba que stackhelx_ports retorna lista de puertos formateada."""
|
|
356
356
|
(port,) = free_ports(1)
|
|
357
357
|
req = {
|
|
358
358
|
"jsonrpc": "2.0",
|
|
@@ -657,7 +657,7 @@ def test_niveles_de_una_cadena_lineal():
|
|
|
657
657
|
|
|
658
658
|
def test_env_file_inyecta_variables(tmp_path, free_ports):
|
|
659
659
|
(port,) = free_ports(1)
|
|
660
|
-
(tmp_path / ".env").write_text("CUSTOM_VAR=
|
|
660
|
+
(tmp_path / ".env").write_text("CUSTOM_VAR=stackhelx_rocks\n", encoding="utf-8")
|
|
661
661
|
flag = tmp_path / "env_result.txt"
|
|
662
662
|
|
|
663
663
|
stack = stack_from(
|
|
@@ -674,7 +674,7 @@ def test_env_file_inyecta_variables(tmp_path, free_ports):
|
|
|
674
674
|
try:
|
|
675
675
|
engine.up()
|
|
676
676
|
assert flag.exists()
|
|
677
|
-
assert flag.read_text(encoding="utf-8") == "
|
|
677
|
+
assert flag.read_text(encoding="utf-8") == "stackhelx_rocks"
|
|
678
678
|
finally:
|
|
679
679
|
engine.down()
|
|
680
680
|
|
|
@@ -726,7 +726,7 @@ def test_pre_start_fallo_aborta_arranque(tmp_path, free_ports):
|
|
|
726
726
|
|
|
727
727
|
@pytest.fixture
|
|
728
728
|
def sin_env_global(tmp_path, monkeypatch):
|
|
729
|
-
"""`build_env` lee `~/.portmaster/env.global
|
|
729
|
+
"""`build_env` lee `~/.stackhelx/env.global` (o `~/.portmaster/env.global`), o sea el disco de quien corre.
|
|
730
730
|
|
|
731
731
|
Las reglas de precedencia dejan las aserciones de abajo a salvo hoy, pero un
|
|
732
732
|
env.global que defina una de las variables que estos tests dan por ausentes
|
|
@@ -1271,3 +1271,41 @@ def test_bun_detectado_arranca_y_abre_el_puerto(tmp_path, free_ports):
|
|
|
1271
1271
|
while time.time() < deadline and not ports.is_free(port):
|
|
1272
1272
|
time.sleep(0.1)
|
|
1273
1273
|
assert ports.is_free(port), "el puerto quedo tomado despues de down()"
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def test_build_env_compatibilidad_stackhelx_y_portmaster(tmp_path, monkeypatch):
|
|
1277
|
+
"""build_env lee ~/.stackhelx/env.global y usa ~/.portmaster/env.global como fallback."""
|
|
1278
|
+
hogar = tmp_path / "home"
|
|
1279
|
+
hogar.mkdir()
|
|
1280
|
+
monkeypatch.setattr(Path, "home", classmethod(lambda cls: hogar))
|
|
1281
|
+
|
|
1282
|
+
srv = config.Service(
|
|
1283
|
+
name="srv",
|
|
1284
|
+
command="echo ok",
|
|
1285
|
+
cwd=tmp_path,
|
|
1286
|
+
port=8080,
|
|
1287
|
+
ready="port",
|
|
1288
|
+
needs=(),
|
|
1289
|
+
env={},
|
|
1290
|
+
detached=False,
|
|
1291
|
+
)
|
|
1292
|
+
|
|
1293
|
+
# 1. Sin archivo
|
|
1294
|
+
env = runner.build_env(srv)
|
|
1295
|
+
assert "MI_GLOBAL" not in env
|
|
1296
|
+
|
|
1297
|
+
# 2. Solo portmaster legacy
|
|
1298
|
+
dir_portmaster = hogar / ".portmaster"
|
|
1299
|
+
dir_portmaster.mkdir()
|
|
1300
|
+
(dir_portmaster / "env.global").write_text("MI_GLOBAL=desde_portmaster\nOTRA_VAR=1\n", encoding="utf-8")
|
|
1301
|
+
env = runner.build_env(srv)
|
|
1302
|
+
assert env.get("MI_GLOBAL") == "desde_portmaster"
|
|
1303
|
+
assert env.get("OTRA_VAR") == "1"
|
|
1304
|
+
|
|
1305
|
+
# 3. StackHelx sobrescribe portmaster legacy
|
|
1306
|
+
dir_stackhelx = hogar / ".stackhelx"
|
|
1307
|
+
dir_stackhelx.mkdir()
|
|
1308
|
+
(dir_stackhelx / "env.global").write_text("MI_GLOBAL=desde_stackhelx\n", encoding="utf-8")
|
|
1309
|
+
env = runner.build_env(srv)
|
|
1310
|
+
assert env.get("MI_GLOBAL") == "desde_stackhelx"
|
|
1311
|
+
|
|
@@ -1024,7 +1024,7 @@ def test_el_chequeo_de_docker_no_corre_en_cada_request(client, tmp_path, monkeyp
|
|
|
1024
1024
|
|
|
1025
1025
|
|
|
1026
1026
|
def _sesion_recuperada(tmp_path, port):
|
|
1027
|
-
"""Simula un reinicio de `
|
|
1027
|
+
"""Simula un reinicio de `stackhelx serve` con el proceso todavia vivo."""
|
|
1028
1028
|
import json
|
|
1029
1029
|
|
|
1030
1030
|
root = tmp_path / "sobreviviente"
|
|
@@ -1279,7 +1279,7 @@ def _tunel_falso(monkeypatch, proc):
|
|
|
1279
1279
|
|
|
1280
1280
|
|
|
1281
1281
|
def test_los_tuneles_se_cierran_al_apagar_el_servidor(monkeypatch, free_ports):
|
|
1282
|
-
"""`
|
|
1282
|
+
"""`stackhelx serve` terminaba y el cliente de tuneles seguia vivo.
|
|
1283
1283
|
|
|
1284
1284
|
El puerto quedaba expuesto a internet, sin nada en pantalla que lo dijera y
|
|
1285
1285
|
sin forma de cerrarlo salvo matar el proceso a mano.
|
|
@@ -2088,7 +2088,7 @@ def test_browse_frecuentes(client, tmp_path, monkeypatch):
|
|
|
2088
2088
|
|
|
2089
2089
|
|
|
2090
2090
|
def test_share_rechaza_el_puerto_propio(client, monkeypatch):
|
|
2091
|
-
"""
|
|
2091
|
+
"""StackHelx no se publica a si mismo.
|
|
2092
2092
|
|
|
2093
2093
|
Detras de ese puerto esta la API que corre los comandos de stack.yaml, o
|
|
2094
2094
|
sea ejecucion arbitraria: el token pasa a ser lo unico entre internet y tu
|
|
@@ -2107,7 +2107,7 @@ def test_share_rechaza_el_puerto_propio(client, monkeypatch):
|
|
|
2107
2107
|
|
|
2108
2108
|
res = client.post("/api/share?port=80")
|
|
2109
2109
|
assert res.status_code == 400
|
|
2110
|
-
assert llamadas == [], "se abrio un tunel hacia el puerto de
|
|
2110
|
+
assert llamadas == [], "se abrio un tunel hacia el puerto de StackHelx"
|
|
2111
2111
|
|
|
2112
2112
|
|
|
2113
2113
|
def test_share_no_confia_en_el_header_host(client, monkeypatch):
|
|
@@ -275,12 +275,12 @@ def test_el_mcp_rechaza_un_puerto_fuera_de_rango():
|
|
|
275
275
|
mcp._execute_tool("stackhelx_share", {"port": 0})
|
|
276
276
|
|
|
277
277
|
|
|
278
|
-
def
|
|
278
|
+
def test_no_se_publica_un_stackhelx_serve(monkeypatch):
|
|
279
279
|
"""El unico choke point de los tuneles se niega a exponer la propia API.
|
|
280
280
|
|
|
281
281
|
Detras de ese puerto esta lo que corre los comandos de stack.yaml. Va aca y
|
|
282
282
|
no en el comando porque por `start_tunnel` pasan los tres caminos: el boton
|
|
283
|
-
de la interfaz, `
|
|
283
|
+
de la interfaz, `stackhelx share` y el CLI.
|
|
284
284
|
|
|
285
285
|
Se afirma el efecto: que no se lance ningun cliente de tuneles.
|
|
286
286
|
"""
|
|
@@ -290,7 +290,7 @@ def test_no_se_publica_un_portmaster_serve(monkeypatch):
|
|
|
290
290
|
tunnel.ports,
|
|
291
291
|
"scan",
|
|
292
292
|
lambda port: tunnel.ports.PortStatus(
|
|
293
|
-
port=port, free=False, pid=123, cmdline="python -m
|
|
293
|
+
port=port, free=False, pid=123, cmdline="python -m stackhelx serve --port 7666"
|
|
294
294
|
),
|
|
295
295
|
)
|
|
296
296
|
|
|
@@ -299,7 +299,7 @@ def test_no_se_publica_un_portmaster_serve(monkeypatch):
|
|
|
299
299
|
assert lanzados == [], "se levanto un cliente de tuneles contra la propia API"
|
|
300
300
|
|
|
301
301
|
|
|
302
|
-
def
|
|
302
|
+
def test_un_puerto_ajeno_no_se_confunde_con_stackhelx(monkeypatch):
|
|
303
303
|
"""La otra mitad: la guarda no puede frenar un tunel legitimo.
|
|
304
304
|
|
|
305
305
|
Sin esto, un `return True` constante pasaria el test de arriba.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stackhelx-1.0.0 → stackhelx-1.0.1}/docs/superpowers/specs/2026-07-29-autodeteccion-design.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|