stackhelx 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stackhelx/__init__.py +2 -0
- stackhelx/__main__.py +4 -0
- stackhelx/browse.py +160 -0
- stackhelx/cli.py +1165 -0
- stackhelx/config.py +448 -0
- stackhelx/detect.py +1053 -0
- stackhelx/docker.py +202 -0
- stackhelx/doctor.py +356 -0
- stackhelx/guardrails.py +42 -0
- stackhelx/history.py +86 -0
- stackhelx/mcp.py +467 -0
- stackhelx/ports.py +392 -0
- stackhelx/registry.py +356 -0
- stackhelx/runner.py +877 -0
- stackhelx/scripts.py +123 -0
- stackhelx/server.py +1786 -0
- stackhelx/tunnel.py +207 -0
- stackhelx/web/app.css +1729 -0
- stackhelx/web/app.js +2425 -0
- stackhelx/web/index.html +378 -0
- stackhelx/web/tokens.css +104 -0
- stackhelx-1.0.0.dist-info/METADATA +323 -0
- stackhelx-1.0.0.dist-info/RECORD +26 -0
- stackhelx-1.0.0.dist-info/WHEEL +4 -0
- stackhelx-1.0.0.dist-info/entry_points.txt +3 -0
- stackhelx-1.0.0.dist-info/licenses/LICENSE +21 -0
stackhelx/scripts.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Ejecucion de scripts y tareas del proyecto."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
from typing import Sequence
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from . import config, registry
|
|
14
|
+
from .config import ConfigError, Stack
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_script_env(stack: Stack) -> dict[str, str]:
|
|
18
|
+
"""Entorno de ejecucion para scripts de proyecto."""
|
|
19
|
+
env = dict(os.environ)
|
|
20
|
+
global_env = registry.HOME / "env.global"
|
|
21
|
+
if global_env.is_file():
|
|
22
|
+
env.update(config.parse_env_file(global_env))
|
|
23
|
+
local_env = stack.root / ".env"
|
|
24
|
+
if local_env.is_file():
|
|
25
|
+
env.update(config.parse_env_file(local_env))
|
|
26
|
+
env["PYTHONUNBUFFERED"] = "1"
|
|
27
|
+
env["FORCE_COLOR"] = "1"
|
|
28
|
+
return env
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _entrecomillar(args: Sequence[str]) -> str:
|
|
32
|
+
"""Une los argumentos extra respetando sus limites, para una linea de shell.
|
|
33
|
+
|
|
34
|
+
Iban con un `" ".join`, y estos comandos corren con `shell=True`: un
|
|
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.
|
|
37
|
+
|
|
38
|
+
Por plataforma, porque el shell no es el mismo. `shlex.join` entrecomilla al
|
|
39
|
+
estilo POSIX y `cmd.exe` no entiende las comillas simples: un `&` adentro le
|
|
40
|
+
seguiria partiendo el comando.
|
|
41
|
+
|
|
42
|
+
En Windows hacen falta las dos capas, y con `list2cmdline` sola no alcanza:
|
|
43
|
+
escapa las comillas dobles como `\\"`, que es la convencion de CreateProcess,
|
|
44
|
+
y `cmd.exe` no la conoce. Ve esa comilla como cierre, lo que sigue queda
|
|
45
|
+
afuera del entrecomillado y un `&` ahi ejecuta un segundo comando. Probado:
|
|
46
|
+
con `a" & echo x > archivo & echo "b` el archivo se creaba.
|
|
47
|
+
|
|
48
|
+
Entonces: `list2cmdline` para la capa de argumentos, y despues `^` delante
|
|
49
|
+
de cada metacaracter de cmd, comillas incluidas. Asi cmd los pasa literales
|
|
50
|
+
y el programa los parsea con sus propias reglas, que es lo que corresponde.
|
|
51
|
+
"""
|
|
52
|
+
if os.name == "nt":
|
|
53
|
+
linea = subprocess.list2cmdline(list(args))
|
|
54
|
+
return _CMD_META.sub(r"^\g<0>", linea)
|
|
55
|
+
return shlex.join(args)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Lo que `cmd.exe` interpreta antes de partir en argumentos. El `%` entra porque
|
|
59
|
+
# expande variables adentro de comillas, que es la otra forma de salirse.
|
|
60
|
+
_CMD_META = re.compile(r'[()%!^"<>&|]')
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def resolve(stack: Stack, name: str, _visto: tuple[str, ...] = ()) -> list[str]:
|
|
64
|
+
"""Aplana un script a comandos de shell, expandiendo los que nombran a otro.
|
|
65
|
+
|
|
66
|
+
Un item que coincide con el nombre de otro script es una referencia; todo lo
|
|
67
|
+
demas es un comando literal. Sin esto, el `check: [lint, test]` que documenta
|
|
68
|
+
`docs/stack-yaml.md` corria `lint` como si fuera un binario y moria con "no
|
|
69
|
+
se reconoce como comando".
|
|
70
|
+
|
|
71
|
+
Un nombre que no es script queda como comando tal cual, que es lo que hace
|
|
72
|
+
falta para `test: pytest -v`. No se puede distinguir un binario real de una
|
|
73
|
+
referencia mal escrita sin adivinar, y adivinar en un ejecutor de comandos
|
|
74
|
+
sale mas caro que el error del shell.
|
|
75
|
+
"""
|
|
76
|
+
if name in _visto:
|
|
77
|
+
raise ConfigError(f"ciclo entre scripts: {' -> '.join((*_visto, name))}")
|
|
78
|
+
fuera: list[str] = []
|
|
79
|
+
for item in stack.scripts[name]:
|
|
80
|
+
if item in stack.scripts:
|
|
81
|
+
fuera.extend(resolve(stack, item, (*_visto, name)))
|
|
82
|
+
else:
|
|
83
|
+
fuera.append(item)
|
|
84
|
+
return fuera
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_script(
|
|
88
|
+
stack: Stack,
|
|
89
|
+
name: str,
|
|
90
|
+
extra_args: Sequence[str] | None = None,
|
|
91
|
+
console: Console | None = None,
|
|
92
|
+
) -> int:
|
|
93
|
+
"""Ejecuta una tarea o pipeline de comandos definido en stack.yaml.
|
|
94
|
+
|
|
95
|
+
Retorna el codigo de salida del comando (0 si todos terminan con exito).
|
|
96
|
+
"""
|
|
97
|
+
console = console or Console()
|
|
98
|
+
if name not in stack.scripts:
|
|
99
|
+
known = ", ".join(stack.scripts) or "ninguno"
|
|
100
|
+
raise ConfigError(f"script desconocido: {name!r}. Definidos: {known}")
|
|
101
|
+
|
|
102
|
+
commands = resolve(stack, name)
|
|
103
|
+
env = build_script_env(stack)
|
|
104
|
+
extra_str = (" " + _entrecomillar(extra_args)) if extra_args else ""
|
|
105
|
+
|
|
106
|
+
for idx, cmd in enumerate(commands):
|
|
107
|
+
# Si es el ultimo comando del pipeline, le pasamos los extra_args
|
|
108
|
+
full_cmd = (cmd + extra_str) if (idx == len(commands) - 1 and extra_str) else cmd
|
|
109
|
+
console.print(f"[bold cyan]$[/] [dim]{full_cmd}[/]")
|
|
110
|
+
res = subprocess.run(
|
|
111
|
+
full_cmd,
|
|
112
|
+
shell=True,
|
|
113
|
+
cwd=stack.root,
|
|
114
|
+
env=env,
|
|
115
|
+
)
|
|
116
|
+
if res.returncode != 0:
|
|
117
|
+
console.print(
|
|
118
|
+
f"[bold red]error:[/] el script '{name}' fallo en el paso {idx + 1} "
|
|
119
|
+
f"con codigo {res.returncode}"
|
|
120
|
+
)
|
|
121
|
+
return res.returncode
|
|
122
|
+
|
|
123
|
+
return 0
|