stackhelx 1.0.1__tar.gz → 1.1.0__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.1 → stackhelx-1.1.0}/CHANGELOG.md +14 -5
- {stackhelx-1.0.1 → stackhelx-1.1.0}/PKG-INFO +13 -6
- {stackhelx-1.0.1 → stackhelx-1.1.0}/README.md +12 -5
- {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/comandos.md +5 -2
- stackhelx-1.1.0/stackhelx/__init__.py +2 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/cli.py +15 -1
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/mcp.py +1 -1
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/runner.py +1 -1
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/server.py +1 -1
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/tunnel.py +1 -1
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/web/app.css +102 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/web/app.js +101 -1
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/web/index.html +30 -1
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_cli.py +34 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_scripts.py +1 -1
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_server.py +33 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_tunnel.py +3 -3
- stackhelx-1.0.1/stackhelx/__init__.py +0 -2
- {stackhelx-1.0.1 → stackhelx-1.1.0}/.github/workflows/ci.yml +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/.github/workflows/release.yml +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/.gitignore +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/AGENTS.md +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/CLAUDE.md +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/LICENSE +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/deteccion.md +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/interfaz.md +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/pendientes.md +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/plan-expansion.md +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/plan-lenguajes.md +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/plan-siguiente.md +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/stack-yaml.md +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/superpowers/plans/2026-08-08-partir-readme.md +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/superpowers/plans/2026-08-08-release-1-0-1.md +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/superpowers/plans/2026-08-18-campo-url.md +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/superpowers/specs/2026-07-29-autodeteccion-design.md +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/pyproject.toml +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/scripts/check_docs.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/scripts/check_metadata.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stack.example.yaml +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/__main__.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/browse.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/config.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/detect.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/docker.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/doctor.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/guardrails.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/history.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/ports.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/registry.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/scripts.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/web/tokens.css +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/conftest.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_browse.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_config.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_conftest.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_detect.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_guardrails.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_history.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_mcp.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_ports.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_registry.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_runner.py +0 -0
- {stackhelx-1.0.1 → stackhelx-1.1.0}/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.1.0] - 2026-09-18
|
|
8
|
+
|
|
9
|
+
### Añadido y Mejorado
|
|
10
|
+
|
|
11
|
+
- **Onboarding de Agentes MCP:** Modal web interactivo con panel desplegable colapsable para configurar clientes de IA (Claude Desktop, Cursor, Antigravity, VS Code), con generación de snippets JSON (`shx` / `uvx`), descarga directa de `stackhelx-mcp.json` y copia de System Prompt para agentes.
|
|
12
|
+
- **Banderas CLI para MCP:** Se incorporan las opciones `shx mcp --config` (imprime bloque JSON de configuración) y `shx mcp --prompt` (imprime instrucciones recomendadas para agentes).
|
|
13
|
+
- **Telemetría y Métricas en Tiempo Real:** Tooltip explicativo (`title` y `aria-label`) con cursor `help` sobre el badge de métricas de servicio (`.service__metrics`). Desglosa el uso de CPU multi-core (equivalencia de núcleos activos) y aclara que la memoria es física residente (RSS).
|
|
14
|
+
- **Documentación del alias `shx`:** `README.md` y `docs/comandos.md` documentan formalmente la instalación y uso del comando abreviado `shx` 1:1 junto con `stackhelx`.
|
|
15
|
+
- **Diseño sin emojis:** Estilo visual puro y tipográfico, conforme a los principios de diseño sobrio y accesible.
|
|
16
|
+
|
|
7
17
|
## [1.0.1] - 2026-09-13
|
|
8
18
|
|
|
9
19
|
### Seguridad y Limpieza
|
|
@@ -558,8 +568,7 @@ Primera versión publicada. Lo que sigue es el alcance completo, no un diff.
|
|
|
558
568
|
[1.3.0]: https://github.com/TicoraX/PortMaster/releases/tag/v1.3.0
|
|
559
569
|
[1.2.0]: https://github.com/TicoraX/PortMaster/releases/tag/v1.2.0
|
|
560
570
|
[1.1.1]: https://github.com/TicoraX/PortMaster/releases/tag/v1.1.1
|
|
561
|
-
[1.1.0]: https://github.com/TicoraX/
|
|
562
|
-
[1.0.
|
|
563
|
-
[1.0.
|
|
564
|
-
|
|
565
|
-
[1.0.0]: https://github.com/TicoraX/PortMaster/releases/tag/v1.0.0
|
|
571
|
+
[1.1.0]: https://github.com/TicoraX/StackHelx/releases/tag/v1.1.0
|
|
572
|
+
[1.0.1]: https://github.com/TicoraX/StackHelx/releases/tag/v1.0.1
|
|
573
|
+
[1.0.0]: https://github.com/TicoraX/StackHelx/releases/tag/v1.0.0
|
|
574
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: stackhelx
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
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
|
|
@@ -59,7 +59,7 @@ Description-Content-Type: text/markdown
|
|
|
59
59
|
|
|
60
60
|
# StackHelx
|
|
61
61
|
|
|
62
|
-
[](https://pypi.org/project/stackhelx/)
|
|
62
|
+
[](https://pypi.org/project/stackhelx/)
|
|
63
63
|
[](https://github.com/TicoraX/StackHelx/actions/workflows/ci.yml)
|
|
64
64
|
[](LICENSE)
|
|
65
65
|
[](pyproject.toml)
|
|
@@ -76,11 +76,15 @@ uv tool install stackhelx
|
|
|
76
76
|
pipx install stackhelx
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
La instalación registra dos ejecutables idénticos en el sistema: el comando principal **`stackhelx`** y su versión abreviada **`shx`**.
|
|
80
|
+
|
|
79
81
|
Requiere Python 3.10 o superior. Funciona en Windows, macOS y Linux.
|
|
80
82
|
|
|
81
83
|
## Comandos
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
Todos los comandos pueden ejecutarse como `stackhelx <comando>` o de forma abreviada con su alias oficial **`shx <comando>`** (ej. `shx up`, `shx down`, `shx doctor`, `shx ports`):
|
|
86
|
+
|
|
87
|
+
| Comando (`stackhelx` / `shx`) | Qué hace |
|
|
84
88
|
|---|---|
|
|
85
89
|
| `stackhelx up` | Levanta el stack entero: libera puertos, arranca en orden y sigue los logs |
|
|
86
90
|
| `stackhelx down` | Baja lo que sobrevive a la terminal, o sea contenedores |
|
|
@@ -113,10 +117,13 @@ Cada uno con `--help`.
|
|
|
113
117
|
## Arrancar un stack
|
|
114
118
|
|
|
115
119
|
```bash
|
|
120
|
+
shx up
|
|
121
|
+
# o
|
|
116
122
|
stackhelx up
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
|
|
124
|
+
shx up --profile backend # solo un subconjunto
|
|
125
|
+
shx up --no-free # no tocar los puertos ocupados
|
|
126
|
+
shx up --env-file .env.qa # carga ese .env antes de arrancar
|
|
120
127
|
```
|
|
121
128
|
|
|
122
129
|
`--env-file` no reemplaza al `env_file:` de `stack.yaml`, se suma: carga el
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# StackHelx
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/stackhelx/)
|
|
3
|
+
[](https://pypi.org/project/stackhelx/)
|
|
4
4
|
[](https://github.com/TicoraX/StackHelx/actions/workflows/ci.yml)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](pyproject.toml)
|
|
@@ -17,11 +17,15 @@ uv tool install stackhelx
|
|
|
17
17
|
pipx install stackhelx
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
La instalación registra dos ejecutables idénticos en el sistema: el comando principal **`stackhelx`** y su versión abreviada **`shx`**.
|
|
21
|
+
|
|
20
22
|
Requiere Python 3.10 o superior. Funciona en Windows, macOS y Linux.
|
|
21
23
|
|
|
22
24
|
## Comandos
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
Todos los comandos pueden ejecutarse como `stackhelx <comando>` o de forma abreviada con su alias oficial **`shx <comando>`** (ej. `shx up`, `shx down`, `shx doctor`, `shx ports`):
|
|
27
|
+
|
|
28
|
+
| Comando (`stackhelx` / `shx`) | Qué hace |
|
|
25
29
|
|---|---|
|
|
26
30
|
| `stackhelx up` | Levanta el stack entero: libera puertos, arranca en orden y sigue los logs |
|
|
27
31
|
| `stackhelx down` | Baja lo que sobrevive a la terminal, o sea contenedores |
|
|
@@ -54,10 +58,13 @@ Cada uno con `--help`.
|
|
|
54
58
|
## Arrancar un stack
|
|
55
59
|
|
|
56
60
|
```bash
|
|
61
|
+
shx up
|
|
62
|
+
# o
|
|
57
63
|
stackhelx up
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
|
|
65
|
+
shx up --profile backend # solo un subconjunto
|
|
66
|
+
shx up --no-free # no tocar los puertos ocupados
|
|
67
|
+
shx up --env-file .env.qa # carga ese .env antes de arrancar
|
|
61
68
|
```
|
|
62
69
|
|
|
63
70
|
`--env-file` no reemplaza al `env_file:` de `stack.yaml`, se suma: carga el
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# Otros comandos
|
|
2
2
|
|
|
3
|
+
> **Nota:** Todos los comandos del sistema pueden ejecutarse indistintamente con `stackhelx <cmd>` o mediante el alias corto oficial **`shx <cmd>`** (por ejemplo: `shx down`, `shx doctor`, `shx switch`, `shx run`, `shx share`).
|
|
4
|
+
|
|
3
5
|
`up`, `serve`, `ports` y `free` están en el [README](../README.md#comandos).
|
|
4
6
|
Acá están los cuatro que quedan, con qué revisa cada uno y por qué.
|
|
5
7
|
|
|
6
8
|
## Bajar lo que sobrevive a la terminal
|
|
7
9
|
|
|
8
10
|
```bash
|
|
9
|
-
|
|
10
|
-
stackhelx down
|
|
11
|
+
shx down
|
|
12
|
+
# o stackhelx down
|
|
13
|
+
shx down --profile backend
|
|
11
14
|
```
|
|
12
15
|
|
|
13
16
|
`Ctrl-C` sobre un `stackhelx up` apaga a sus hijos, pero un
|
|
@@ -892,19 +892,33 @@ def mcp_cmd(
|
|
|
892
892
|
"-c",
|
|
893
893
|
help="Muestra el bloque de configuracion JSON para Claude Desktop, Cursor o Antigravity.",
|
|
894
894
|
),
|
|
895
|
+
show_prompt: bool = typer.Option(
|
|
896
|
+
False,
|
|
897
|
+
"--prompt",
|
|
898
|
+
"-p",
|
|
899
|
+
help="Muestra las instrucciones recomendadas para el agente de IA.",
|
|
900
|
+
),
|
|
895
901
|
) -> None:
|
|
896
902
|
"""Inicia el servidor Model Context Protocol (MCP) sobre stdio para agentes de IA."""
|
|
897
903
|
if show_config:
|
|
898
904
|
cfg = {
|
|
899
905
|
"mcpServers": {
|
|
900
906
|
"stackhelx": {
|
|
901
|
-
"command": "
|
|
907
|
+
"command": "shx",
|
|
902
908
|
"args": ["mcp"],
|
|
903
909
|
}
|
|
904
910
|
}
|
|
905
911
|
}
|
|
906
912
|
console.print(json.dumps(cfg, indent=2))
|
|
907
913
|
return
|
|
914
|
+
if show_prompt:
|
|
915
|
+
console.print(
|
|
916
|
+
"Tienes a tu disposición las herramientas MCP de StackHelx (`stackhelx_*`). "
|
|
917
|
+
"Úsalas para inspeccionar el estado de puertos (`stackhelx_ports`), diagnosticar problemas de entorno (`stackhelx_doctor`), "
|
|
918
|
+
"liberar puertos en conflicto (`stackhelx_free`), levantar el stack del proyecto (`stackhelx_up`), "
|
|
919
|
+
"apagarlo (`stackhelx_down`), ejecutar scripts declarados (`stackhelx_run`) y compartir servicios vía túneles HTTPS (`stackhelx_share`)."
|
|
920
|
+
)
|
|
921
|
+
return
|
|
908
922
|
mcp.serve_stdio()
|
|
909
923
|
|
|
910
924
|
|
|
@@ -420,7 +420,7 @@ def _reservar_stdout():
|
|
|
420
420
|
"""Deja el descriptor 1 solo para el protocolo y manda lo demas a stderr.
|
|
421
421
|
|
|
422
422
|
Sobre stdio el JSON-RPC comparte el descriptor 1 con todo lo que imprima el
|
|
423
|
-
proceso. `portmaster_run` lanza los comandos del usuario heredando ese
|
|
423
|
+
proceso. `stackhelx_run` (o `portmaster_run`) lanza los comandos del usuario heredando ese
|
|
424
424
|
descriptor, asi que un `echo` adentro de un script se metia entre dos
|
|
425
425
|
respuestas y el cliente perdia la sesion.
|
|
426
426
|
|
|
@@ -348,7 +348,7 @@ class Runner:
|
|
|
348
348
|
if estrenados:
|
|
349
349
|
# Un solo respiro para todo el lote, no uno por proceso: sin esto,
|
|
350
350
|
# la primera lectura de un servicio recien arrancado seria 0.0 y
|
|
351
|
-
# `
|
|
351
|
+
# `stackhelx stats`, que hace una sola consulta, nunca mediria nada.
|
|
352
352
|
time.sleep(CPU_MUESTRA)
|
|
353
353
|
|
|
354
354
|
vivos = {proc.pid for arbol in arboles.values() for proc in arbol}
|
|
@@ -695,7 +695,7 @@ def _cerrar_tuneles() -> None:
|
|
|
695
695
|
async def _ciclo_de_vida(app: FastAPI):
|
|
696
696
|
"""Al apagar, cierra los tuneles.
|
|
697
697
|
|
|
698
|
-
Sin esto `
|
|
698
|
+
Sin esto `stackhelx serve` terminaba y el cliente de tuneles seguia vivo,
|
|
699
699
|
con el puerto expuesto a internet, sin nada en pantalla que lo dijera y sin
|
|
700
700
|
forma de cerrarlo que no fuera matarlo a mano.
|
|
701
701
|
"""
|
|
@@ -146,7 +146,7 @@ def start_tunnel(
|
|
|
146
146
|
|
|
147
147
|
# Mirando tambien si el proceso se murio, y no solo el reloj. Un cliente que
|
|
148
148
|
# falla al arrancar (`ngrok` sin autenticar) se va en menos de un segundo, y
|
|
149
|
-
# esperarle el plazo entero dejaba a `
|
|
149
|
+
# esperarle el plazo entero dejaba a `stackhelx share` pareciendo colgado
|
|
150
150
|
# antes de dar un error que ya se sabia.
|
|
151
151
|
limite = time.monotonic() + timeout
|
|
152
152
|
while not ready_event.wait(0.1):
|
|
@@ -635,6 +635,7 @@ body {
|
|
|
635
635
|
border-radius: var(--radius-sm);
|
|
636
636
|
white-space: nowrap;
|
|
637
637
|
align-self: center;
|
|
638
|
+
cursor: help;
|
|
638
639
|
}
|
|
639
640
|
|
|
640
641
|
.service__act {
|
|
@@ -1653,6 +1654,107 @@ body {
|
|
|
1653
1654
|
margin-top: var(--space-sm);
|
|
1654
1655
|
}
|
|
1655
1656
|
|
|
1657
|
+
.mcp__setup {
|
|
1658
|
+
border: var(--rule) solid var(--color-rule);
|
|
1659
|
+
border-radius: var(--radius);
|
|
1660
|
+
background: var(--color-paper-2);
|
|
1661
|
+
overflow: hidden;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
.mcp__setup-summary {
|
|
1665
|
+
display: flex;
|
|
1666
|
+
align-items: center;
|
|
1667
|
+
justify-content: space-between;
|
|
1668
|
+
padding: var(--space-xs) var(--space-sm);
|
|
1669
|
+
font-size: var(--text-sm);
|
|
1670
|
+
font-weight: 500;
|
|
1671
|
+
color: var(--color-ink-2);
|
|
1672
|
+
cursor: pointer;
|
|
1673
|
+
user-select: none;
|
|
1674
|
+
list-style: none;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
.mcp__setup-summary::-webkit-details-marker {
|
|
1678
|
+
display: none;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
.mcp__setup-summary:hover {
|
|
1682
|
+
color: var(--color-ink);
|
|
1683
|
+
background: var(--color-paper-3);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
.mcp__setup-summary:focus-visible {
|
|
1687
|
+
outline: 2px solid var(--color-focus);
|
|
1688
|
+
outline-offset: -2px;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
.mcp__setup-icon {
|
|
1692
|
+
font-size: var(--text-xs);
|
|
1693
|
+
transition: transform 0.15s ease;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
.mcp__setup[open] .mcp__setup-icon {
|
|
1697
|
+
transform: rotate(180deg);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
.mcp__setup-body {
|
|
1701
|
+
padding: var(--space-sm) var(--space-md) var(--space-md);
|
|
1702
|
+
border-top: var(--rule) solid var(--color-rule);
|
|
1703
|
+
display: flex;
|
|
1704
|
+
flex-direction: column;
|
|
1705
|
+
gap: var(--space-xs);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
.mcp__setup-tabs {
|
|
1709
|
+
display: flex;
|
|
1710
|
+
gap: var(--space-md);
|
|
1711
|
+
font-size: var(--text-xs);
|
|
1712
|
+
color: var(--color-ink-2);
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
.mcp__setup-radio {
|
|
1716
|
+
display: inline-flex;
|
|
1717
|
+
align-items: center;
|
|
1718
|
+
gap: 0.35rem;
|
|
1719
|
+
cursor: pointer;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
.mcp__code {
|
|
1723
|
+
font-family: var(--font-mono);
|
|
1724
|
+
font-size: var(--text-xs);
|
|
1725
|
+
background: var(--color-paper);
|
|
1726
|
+
border: var(--rule) solid var(--color-rule);
|
|
1727
|
+
border-radius: var(--radius-sm);
|
|
1728
|
+
padding: var(--space-xs) var(--space-sm);
|
|
1729
|
+
margin: 0;
|
|
1730
|
+
white-space: pre;
|
|
1731
|
+
overflow-x: auto;
|
|
1732
|
+
color: var(--color-ink);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
.mcp__setup-actions {
|
|
1736
|
+
display: flex;
|
|
1737
|
+
flex-wrap: wrap;
|
|
1738
|
+
gap: var(--space-xs);
|
|
1739
|
+
margin-top: 0.25rem;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
.mcp__setup-hint {
|
|
1743
|
+
font-size: var(--text-xs);
|
|
1744
|
+
color: var(--color-ink-3);
|
|
1745
|
+
margin: 0.25rem 0 0;
|
|
1746
|
+
line-height: 1.4;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
.mcp__setup-hint code {
|
|
1750
|
+
font-family: var(--font-mono);
|
|
1751
|
+
color: var(--color-ink-2);
|
|
1752
|
+
background: var(--color-paper);
|
|
1753
|
+
padding: 0.1rem 0.3rem;
|
|
1754
|
+
border-radius: 3px;
|
|
1755
|
+
border: var(--rule) solid var(--color-rule);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1656
1758
|
.mcp__stats {
|
|
1657
1759
|
display: flex;
|
|
1658
1760
|
flex-wrap: wrap;
|
|
@@ -52,6 +52,11 @@ const ui = {
|
|
|
52
52
|
mcpQuotaUsed: document.getElementById("mcp-quota-used"),
|
|
53
53
|
mcpBreakdown: document.getElementById("mcp-breakdown"),
|
|
54
54
|
mcpTbody: document.getElementById("mcp-tbody"),
|
|
55
|
+
mcpSetup: document.getElementById("mcp-setup"),
|
|
56
|
+
mcpJsonSnippet: document.getElementById("mcp-json-snippet"),
|
|
57
|
+
btnMcpCopyJson: document.getElementById("btn-mcp-copy-json"),
|
|
58
|
+
btnMcpDownloadJson: document.getElementById("btn-mcp-download-json"),
|
|
59
|
+
btnMcpCopyPrompt: document.getElementById("btn-mcp-copy-prompt"),
|
|
55
60
|
};
|
|
56
61
|
|
|
57
62
|
const TITLE = document.title;
|
|
@@ -993,8 +998,15 @@ function updateCard(entry, project) {
|
|
|
993
998
|
if (badge && metrics[s.name]) {
|
|
994
999
|
const m = metrics[s.name];
|
|
995
1000
|
if (m.memory_mb > 0 || m.cpu_percent > 0) {
|
|
1001
|
+
const coresEquiv = (m.cpu_percent / 100).toFixed(1);
|
|
1002
|
+
const cpuInfo = m.cpu_percent > 100
|
|
1003
|
+
? `CPU: ${m.cpu_percent}% (~${coresEquiv} núcleos en paralelo)`
|
|
1004
|
+
: `CPU: ${m.cpu_percent}% de 1 núcleo`;
|
|
1005
|
+
const memInfo = `RAM: ${m.memory_mb} MB (memoria física residente RSS)`;
|
|
1006
|
+
|
|
996
1007
|
badge.textContent = `${m.cpu_percent}% · ${m.memory_mb} MB`;
|
|
997
|
-
badge.
|
|
1008
|
+
badge.title = `${cpuInfo}\n${memInfo}`;
|
|
1009
|
+
badge.setAttribute("aria-label", `${cpuInfo}, ${memInfo}`);
|
|
998
1010
|
badge.hidden = false;
|
|
999
1011
|
} else {
|
|
1000
1012
|
badge.hidden = true;
|
|
@@ -2298,9 +2310,47 @@ async function refreshPortsModal() {
|
|
|
2298
2310
|
|
|
2299
2311
|
/* mcp modal ----------------------------------------------------------------- */
|
|
2300
2312
|
|
|
2313
|
+
const MCP_CONFIGS = {
|
|
2314
|
+
shx: JSON.stringify(
|
|
2315
|
+
{
|
|
2316
|
+
mcpServers: {
|
|
2317
|
+
stackhelx: {
|
|
2318
|
+
command: "shx",
|
|
2319
|
+
args: ["mcp"],
|
|
2320
|
+
},
|
|
2321
|
+
},
|
|
2322
|
+
},
|
|
2323
|
+
null,
|
|
2324
|
+
2,
|
|
2325
|
+
),
|
|
2326
|
+
uvx: JSON.stringify(
|
|
2327
|
+
{
|
|
2328
|
+
mcpServers: {
|
|
2329
|
+
stackhelx: {
|
|
2330
|
+
command: "uvx",
|
|
2331
|
+
args: ["stackhelx", "mcp"],
|
|
2332
|
+
},
|
|
2333
|
+
},
|
|
2334
|
+
},
|
|
2335
|
+
null,
|
|
2336
|
+
2,
|
|
2337
|
+
),
|
|
2338
|
+
};
|
|
2339
|
+
|
|
2340
|
+
const MCP_AGENT_PROMPT =
|
|
2341
|
+
"Tienes a tu disposición las herramientas MCP de StackHelx (`stackhelx_*`). Úsalas para inspeccionar el estado de puertos (`stackhelx_ports`), diagnosticar problemas de entorno (`stackhelx_doctor`), liberar puertos tomados (`stackhelx_free`), levantar el stack del proyecto (`stackhelx_up`), apagarlo (`stackhelx_down`), ejecutar scripts declarados (`stackhelx_run`) y compartir servicios vía túneles HTTPS (`stackhelx_share`).";
|
|
2342
|
+
|
|
2343
|
+
function updateMcpSnippet() {
|
|
2344
|
+
if (!ui.mcpJsonSnippet) return;
|
|
2345
|
+
const checked = document.querySelector('input[name="mcp-cmd-type"]:checked');
|
|
2346
|
+
const mode = checked ? checked.value : "shx";
|
|
2347
|
+
ui.mcpJsonSnippet.textContent = MCP_CONFIGS[mode] || MCP_CONFIGS.shx;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2301
2350
|
if (ui.btnMcpModal && ui.mcpModal) {
|
|
2302
2351
|
ui.btnMcpModal.addEventListener("click", () => {
|
|
2303
2352
|
ui.mcpModal.showModal();
|
|
2353
|
+
updateMcpSnippet();
|
|
2304
2354
|
refreshMcpModal();
|
|
2305
2355
|
});
|
|
2306
2356
|
const closeBtn = ui.mcpModal.querySelector('[data-mcp-modal="close"]');
|
|
@@ -2309,7 +2359,57 @@ if (ui.btnMcpModal && ui.mcpModal) {
|
|
|
2309
2359
|
ui.mcpModal.close();
|
|
2310
2360
|
});
|
|
2311
2361
|
}
|
|
2362
|
+
|
|
2363
|
+
document.querySelectorAll('input[name="mcp-cmd-type"]').forEach((radio) => {
|
|
2364
|
+
radio.addEventListener("change", updateMcpSnippet);
|
|
2365
|
+
});
|
|
2366
|
+
|
|
2367
|
+
if (ui.btnMcpCopyJson) {
|
|
2368
|
+
ui.btnMcpCopyJson.addEventListener("click", () => {
|
|
2369
|
+
const text = ui.mcpJsonSnippet ? ui.mcpJsonSnippet.textContent : "";
|
|
2370
|
+
if (navigator.clipboard && text) {
|
|
2371
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
2372
|
+
const orig = ui.btnMcpCopyJson.textContent;
|
|
2373
|
+
ui.btnMcpCopyJson.textContent = "Copiado";
|
|
2374
|
+
setTimeout(() => {
|
|
2375
|
+
ui.btnMcpCopyJson.textContent = orig;
|
|
2376
|
+
}, 1600);
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
if (ui.btnMcpDownloadJson) {
|
|
2383
|
+
ui.btnMcpDownloadJson.addEventListener("click", () => {
|
|
2384
|
+
const text = ui.mcpJsonSnippet ? ui.mcpJsonSnippet.textContent : "";
|
|
2385
|
+
if (!text) return;
|
|
2386
|
+
const blob = new Blob([text], { type: "application/json" });
|
|
2387
|
+
const url = URL.createObjectURL(blob);
|
|
2388
|
+
const a = document.createElement("a");
|
|
2389
|
+
a.href = url;
|
|
2390
|
+
a.download = "stackhelx-mcp.json";
|
|
2391
|
+
document.body.append(a);
|
|
2392
|
+
a.click();
|
|
2393
|
+
a.remove();
|
|
2394
|
+
URL.revokeObjectURL(url);
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
if (ui.btnMcpCopyPrompt) {
|
|
2399
|
+
ui.btnMcpCopyPrompt.addEventListener("click", () => {
|
|
2400
|
+
if (navigator.clipboard) {
|
|
2401
|
+
navigator.clipboard.writeText(MCP_AGENT_PROMPT).then(() => {
|
|
2402
|
+
const orig = ui.btnMcpCopyPrompt.textContent;
|
|
2403
|
+
ui.btnMcpCopyPrompt.textContent = "Copiado";
|
|
2404
|
+
setTimeout(() => {
|
|
2405
|
+
ui.btnMcpCopyPrompt.textContent = orig;
|
|
2406
|
+
}, 1600);
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2312
2411
|
}
|
|
2412
|
+
updateMcpSnippet();
|
|
2313
2413
|
|
|
2314
2414
|
async function refreshMcpModal() {
|
|
2315
2415
|
if (!ui.mcpModal) return;
|
|
@@ -166,10 +166,39 @@
|
|
|
166
166
|
|
|
167
167
|
<dialog class="picker mcp-modal" id="mcp-modal" aria-label="Telemetría de Agentes MCP">
|
|
168
168
|
<div class="picker__head">
|
|
169
|
-
<p class="picker__path">
|
|
169
|
+
<p class="picker__path">Servidor MCP para Agentes</p>
|
|
170
170
|
<button class="btn btn--quiet" type="button" data-mcp-modal="close">Cerrar</button>
|
|
171
171
|
</div>
|
|
172
172
|
<div class="mcp__content">
|
|
173
|
+
<details class="mcp__setup" id="mcp-setup">
|
|
174
|
+
<summary class="mcp__setup-summary">
|
|
175
|
+
<span>Configuración y Prompt para Agentes</span>
|
|
176
|
+
<span class="mcp__setup-icon" aria-hidden="true">▾</span>
|
|
177
|
+
</summary>
|
|
178
|
+
<div class="mcp__setup-body">
|
|
179
|
+
<div class="mcp__setup-tabs">
|
|
180
|
+
<label class="mcp__setup-radio">
|
|
181
|
+
<input type="radio" name="mcp-cmd-type" value="shx" checked>
|
|
182
|
+
<span>shx (recomendado)</span>
|
|
183
|
+
</label>
|
|
184
|
+
<label class="mcp__setup-radio">
|
|
185
|
+
<input type="radio" name="mcp-cmd-type" value="uvx">
|
|
186
|
+
<span>uvx (sin instalación)</span>
|
|
187
|
+
</label>
|
|
188
|
+
</div>
|
|
189
|
+
<pre class="mcp__code" id="mcp-json-snippet"></pre>
|
|
190
|
+
<div class="mcp__setup-actions">
|
|
191
|
+
<button class="btn btn--quiet" type="button" id="btn-mcp-copy-json">Copiar JSON</button>
|
|
192
|
+
<button class="btn btn--quiet" type="button" id="btn-mcp-download-json">Descargar .json</button>
|
|
193
|
+
<button class="btn btn--quiet" type="button" id="btn-mcp-copy-prompt">Copiar Prompt</button>
|
|
194
|
+
</div>
|
|
195
|
+
<p class="mcp__setup-hint">
|
|
196
|
+
<strong>Claude Desktop (Windows):</strong> <code>%APPDATA%\Claude\claude_desktop_config.json</code><br>
|
|
197
|
+
<strong>Cursor:</strong> <code>.cursor/mcp.json</code>
|
|
198
|
+
</p>
|
|
199
|
+
</div>
|
|
200
|
+
</details>
|
|
201
|
+
|
|
173
202
|
<div class="mcp__stats">
|
|
174
203
|
<span class="mcp__stat">Llamadas totales: <strong id="mcp-total-calls">0</strong></span>
|
|
175
204
|
<span class="mcp__stat">Cuota activa: <strong id="mcp-quota-used">0/30</strong> req/min</span>
|
|
@@ -992,3 +992,37 @@ def test_mcp_status_cli():
|
|
|
992
992
|
assert "stackhelx_status" in resultado.output
|
|
993
993
|
|
|
994
994
|
|
|
995
|
+
def test_project_scripts_register_stackhelx_and_shx():
|
|
996
|
+
"""pyproject.toml debe registrar tanto 'stackhelx' como el alias corto 'shx'."""
|
|
997
|
+
from pathlib import Path
|
|
998
|
+
|
|
999
|
+
pyproject_path = Path(__file__).resolve().parent.parent / "pyproject.toml"
|
|
1000
|
+
try:
|
|
1001
|
+
import tomllib
|
|
1002
|
+
|
|
1003
|
+
with open(pyproject_path, "rb") as f:
|
|
1004
|
+
data = tomllib.load(f)
|
|
1005
|
+
scripts = data["project"]["scripts"]
|
|
1006
|
+
assert scripts.get("stackhelx") == "stackhelx.cli:app"
|
|
1007
|
+
assert scripts.get("shx") == "stackhelx.cli:app"
|
|
1008
|
+
except ImportError:
|
|
1009
|
+
texto = pyproject_path.read_text(encoding="utf-8")
|
|
1010
|
+
assert 'stackhelx = "stackhelx.cli:app"' in texto
|
|
1011
|
+
assert 'shx = "stackhelx.cli:app"' in texto
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def test_mcp_config_and_prompt_cli():
|
|
1015
|
+
"""El comando mcp --config y --prompt imprimen el JSON y las instrucciones del agente."""
|
|
1016
|
+
res_cfg = runner.invoke(cli.app, ["mcp", "--config"])
|
|
1017
|
+
assert res_cfg.exit_code == 0
|
|
1018
|
+
assert "mcpServers" in res_cfg.output
|
|
1019
|
+
assert "stackhelx" in res_cfg.output
|
|
1020
|
+
|
|
1021
|
+
res_prompt = runner.invoke(cli.app, ["mcp", "--prompt"])
|
|
1022
|
+
assert res_prompt.exit_code == 0
|
|
1023
|
+
assert "stackhelx_*" in res_prompt.output
|
|
1024
|
+
assert "stackhelx_ports" in res_prompt.output
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
|
|
@@ -174,7 +174,7 @@ def test_un_ciclo_entre_scripts_se_detecta(tmp_path):
|
|
|
174
174
|
def test_un_argumento_extra_no_ejecuta_un_segundo_comando(tmp_path):
|
|
175
175
|
"""Los extra_args iban con un `" ".join` y esto corre con `shell=True`.
|
|
176
176
|
|
|
177
|
-
Desde el CLI el argumento lo escribe el usuario, pero `portmaster_run` los
|
|
177
|
+
Desde el CLI el argumento lo escribe el usuario, pero `stackhelx_run` (o `portmaster_run`) los
|
|
178
178
|
recibe de un agente de IA: un separador adentro de un argumento ejecutaba lo
|
|
179
179
|
que viniera despues.
|
|
180
180
|
|
|
@@ -2278,3 +2278,36 @@ def test_cache_invalidation_race_no_reinyecta_viejo(tmp_path):
|
|
|
2278
2278
|
|
|
2279
2279
|
with server._stack_lock:
|
|
2280
2280
|
assert clave not in server._stack_seen
|
|
2281
|
+
|
|
2282
|
+
|
|
2283
|
+
def test_metrics_badge_has_tooltip_and_cursor_help():
|
|
2284
|
+
"""El badge de consumo debe proveer tooltip explicativo (title) y cursor help."""
|
|
2285
|
+
js = (server.WEB / "app.js").read_text(encoding="utf-8")
|
|
2286
|
+
assert "badge.title = `${cpuInfo}\\n${memInfo}`" in js
|
|
2287
|
+
assert "núcleos en paralelo" in js
|
|
2288
|
+
assert "memoria física residente RSS" in js
|
|
2289
|
+
|
|
2290
|
+
css = (server.WEB / "app.css").read_text(encoding="utf-8")
|
|
2291
|
+
assert ".service__metrics" in css
|
|
2292
|
+
assert "cursor: help" in css
|
|
2293
|
+
|
|
2294
|
+
|
|
2295
|
+
def test_mcp_setup_collapsible_details():
|
|
2296
|
+
"""El modal de MCP debe tener una sección desplegable para configuración y prompt."""
|
|
2297
|
+
html = (server.WEB / "index.html").read_text(encoding="utf-8")
|
|
2298
|
+
assert '<details class="mcp__setup" id="mcp-setup">' in html
|
|
2299
|
+
assert 'id="btn-mcp-copy-json"' in html
|
|
2300
|
+
assert 'id="btn-mcp-download-json"' in html
|
|
2301
|
+
assert 'id="btn-mcp-copy-prompt"' in html
|
|
2302
|
+
|
|
2303
|
+
js = (server.WEB / "app.js").read_text(encoding="utf-8")
|
|
2304
|
+
assert "btnMcpCopyJson" in js
|
|
2305
|
+
assert "btnMcpDownloadJson" in js
|
|
2306
|
+
assert "btnMcpCopyPrompt" in js
|
|
2307
|
+
assert "stackhelx-mcp.json" in js
|
|
2308
|
+
|
|
2309
|
+
css = (server.WEB / "app.css").read_text(encoding="utf-8")
|
|
2310
|
+
assert ".mcp__setup" in css
|
|
2311
|
+
assert ".mcp__setup-summary" in css
|
|
2312
|
+
|
|
2313
|
+
|
|
@@ -199,7 +199,7 @@ def test_start_tunnel_no_espera_a_un_cliente_que_ya_murio(proveedor_falso):
|
|
|
199
199
|
"""`ngrok` sin autenticar imprime el error y se va en menos de un segundo.
|
|
200
200
|
|
|
201
201
|
Esperar el timeout entero ahi es tiempo regalado: el proceso esta muerto y
|
|
202
|
-
la URL no va a aparecer nunca. Con 15s por defecto, `
|
|
202
|
+
la URL no va a aparecer nunca. Con 15s por defecto, `stackhelx share`
|
|
203
203
|
parecia colgado antes de dar un error que ya se sabia.
|
|
204
204
|
"""
|
|
205
205
|
proveedor_falso("ngrok", ["ERR authentication failed"], vivo=False)
|
|
@@ -229,7 +229,7 @@ def test_tailscale_no_confunde_un_enlace_del_log_con_el_tunel():
|
|
|
229
229
|
|
|
230
230
|
|
|
231
231
|
def test_el_mcp_cierra_sus_tuneles_al_terminar_la_sesion(tmp_path, proveedor_falso):
|
|
232
|
-
"""`
|
|
232
|
+
"""`stackhelx_share` abria el tunel y nadie lo cerraba nunca.
|
|
233
233
|
|
|
234
234
|
Es el mismo agujero que `server._ciclo_de_vida` ya documenta para la
|
|
235
235
|
interfaz: la sesion terminaba y el cliente seguia vivo, con el puerto
|
|
@@ -242,7 +242,7 @@ def test_el_mcp_cierra_sus_tuneles_al_terminar_la_sesion(tmp_path, proveedor_fal
|
|
|
242
242
|
proveedor_falso("cloudflared", lineas)
|
|
243
243
|
mcp.cerrar_tuneles() # arrancar de cero: el registro es de modulo
|
|
244
244
|
|
|
245
|
-
# `
|
|
245
|
+
# `stackhelx_share` solo publica puertos que el proyecto declara, asi que
|
|
246
246
|
# el test necesita un proyecto. Antes corria contra el cwd, que es este
|
|
247
247
|
# repo, que no tiene stack.yaml: la validacion nueva lo dejo en rojo y con
|
|
248
248
|
# el se cayo la unica prueba de que la sesion no deja el cliente vivo.
|
|
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.1 → stackhelx-1.1.0}/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
|