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.
Files changed (63) hide show
  1. {stackhelx-1.0.1 → stackhelx-1.1.0}/CHANGELOG.md +14 -5
  2. {stackhelx-1.0.1 → stackhelx-1.1.0}/PKG-INFO +13 -6
  3. {stackhelx-1.0.1 → stackhelx-1.1.0}/README.md +12 -5
  4. {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/comandos.md +5 -2
  5. stackhelx-1.1.0/stackhelx/__init__.py +2 -0
  6. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/cli.py +15 -1
  7. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/mcp.py +1 -1
  8. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/runner.py +1 -1
  9. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/server.py +1 -1
  10. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/tunnel.py +1 -1
  11. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/web/app.css +102 -0
  12. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/web/app.js +101 -1
  13. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/web/index.html +30 -1
  14. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_cli.py +34 -0
  15. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_scripts.py +1 -1
  16. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_server.py +33 -0
  17. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_tunnel.py +3 -3
  18. stackhelx-1.0.1/stackhelx/__init__.py +0 -2
  19. {stackhelx-1.0.1 → stackhelx-1.1.0}/.github/workflows/ci.yml +0 -0
  20. {stackhelx-1.0.1 → stackhelx-1.1.0}/.github/workflows/release.yml +0 -0
  21. {stackhelx-1.0.1 → stackhelx-1.1.0}/.gitignore +0 -0
  22. {stackhelx-1.0.1 → stackhelx-1.1.0}/AGENTS.md +0 -0
  23. {stackhelx-1.0.1 → stackhelx-1.1.0}/CLAUDE.md +0 -0
  24. {stackhelx-1.0.1 → stackhelx-1.1.0}/LICENSE +0 -0
  25. {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/deteccion.md +0 -0
  26. {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/interfaz.md +0 -0
  27. {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/pendientes.md +0 -0
  28. {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/plan-expansion.md +0 -0
  29. {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/plan-lenguajes.md +0 -0
  30. {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/plan-siguiente.md +0 -0
  31. {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/stack-yaml.md +0 -0
  32. {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/superpowers/plans/2026-08-08-partir-readme.md +0 -0
  33. {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/superpowers/plans/2026-08-08-release-1-0-1.md +0 -0
  34. {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/superpowers/plans/2026-08-18-campo-url.md +0 -0
  35. {stackhelx-1.0.1 → stackhelx-1.1.0}/docs/superpowers/specs/2026-07-29-autodeteccion-design.md +0 -0
  36. {stackhelx-1.0.1 → stackhelx-1.1.0}/pyproject.toml +0 -0
  37. {stackhelx-1.0.1 → stackhelx-1.1.0}/scripts/check_docs.py +0 -0
  38. {stackhelx-1.0.1 → stackhelx-1.1.0}/scripts/check_metadata.py +0 -0
  39. {stackhelx-1.0.1 → stackhelx-1.1.0}/stack.example.yaml +0 -0
  40. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/__main__.py +0 -0
  41. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/browse.py +0 -0
  42. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/config.py +0 -0
  43. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/detect.py +0 -0
  44. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/docker.py +0 -0
  45. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/doctor.py +0 -0
  46. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/guardrails.py +0 -0
  47. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/history.py +0 -0
  48. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/ports.py +0 -0
  49. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/registry.py +0 -0
  50. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/scripts.py +0 -0
  51. {stackhelx-1.0.1 → stackhelx-1.1.0}/stackhelx/web/tokens.css +0 -0
  52. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/conftest.py +0 -0
  53. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_browse.py +0 -0
  54. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_config.py +0 -0
  55. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_conftest.py +0 -0
  56. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_detect.py +0 -0
  57. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_guardrails.py +0 -0
  58. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_history.py +0 -0
  59. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_mcp.py +0 -0
  60. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_ports.py +0 -0
  61. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_registry.py +0 -0
  62. {stackhelx-1.0.1 → stackhelx-1.1.0}/tests/test_runner.py +0 -0
  63. {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/PortMaster/releases/tag/v1.1.0
562
- [1.0.3]: https://github.com/TicoraX/PortMaster/releases/tag/v1.0.3
563
- [1.0.2]: https://github.com/TicoraX/PortMaster/releases/tag/v1.0.2
564
- [1.0.1]: https://github.com/TicoraX/PortMaster/releases/tag/v1.0.1
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.1
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
- [![pypi](https://img.shields.io/pypi/v/stackhelx)](https://pypi.org/project/stackhelx/)
62
+ [![pypi](https://img.shields.io/pypi/v/stackhelx?logo=pypi&logoColor=white)](https://pypi.org/project/stackhelx/)
63
63
  [![tests](https://github.com/TicoraX/StackHelx/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/TicoraX/StackHelx/actions/workflows/ci.yml)
64
64
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
65
65
  [![python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](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
- | Comando | Qué hace |
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
- stackhelx up --profile backend # solo un subconjunto
118
- stackhelx up --no-free # no tocar los puertos ocupados
119
- stackhelx up --env-file .env.qa # carga ese .env antes de arrancar
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
- [![pypi](https://img.shields.io/pypi/v/stackhelx)](https://pypi.org/project/stackhelx/)
3
+ [![pypi](https://img.shields.io/pypi/v/stackhelx?logo=pypi&logoColor=white)](https://pypi.org/project/stackhelx/)
4
4
  [![tests](https://github.com/TicoraX/StackHelx/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/TicoraX/StackHelx/actions/workflows/ci.yml)
5
5
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
6
  [![python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](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
- | Comando | Qué hace |
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
- stackhelx up --profile backend # solo un subconjunto
59
- stackhelx up --no-free # no tocar los puertos ocupados
60
- stackhelx up --env-file .env.qa # carga ese .env antes de arrancar
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
- stackhelx down
10
- stackhelx down --profile backend
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
@@ -0,0 +1,2 @@
1
+ __version__ = "1.1.0"
2
+
@@ -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": "stackhelx",
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
- # `portmaster stats`, que hace una sola consulta, nunca mediria nada.
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 `portmaster serve` terminaba y el cliente de tuneles seguia vivo,
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 `portmaster share` pareciendo colgado
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.setAttribute("aria-label", `CPU: ${m.cpu_percent}%, Memoria: ${m.memory_mb} MB`);
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">Telemetría de Agentes MCP</p>
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, `portmaster share`
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
- """`portmaster_share` abria el tunel y nadie lo cerraba nunca.
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
- # `portmaster_share` solo publica puertos que el proyecto declara, asi que
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.
@@ -1,2 +0,0 @@
1
- __version__ = "1.0.1"
2
-
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