Metadata-Version: 2.5
Name: gicc-hprc-cli
Version: 1.1.0
Summary: CLI del clúster HPRC del GICC: autenticación, proyectos, experimentos y jobs de Slurm desde la terminal.
Project-URL: Homepage, https://gicc.ai
Author-email: GICC — Universidad San Ignacio de Loyola <gicc@usil.pe>
Maintainer-email: GICC — Universidad San Ignacio de Loyola <gicc@usil.pe>
License-Expression: MIT
License-File: LICENSE
Keywords: cli,gicc,hpc,slurm,supabase,usil
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Education
Classifier: Intended Audience :: Science/Research
Classifier: Natural Language :: Spanish
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: System :: Distributed Computing
Classifier: Topic :: Utilities
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: platformdirs>=4.2
Requires-Dist: pyyaml>=6.0
Requires-Dist: questionary>=2.0
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.15
Provides-Extra: dev
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# GICC HPRC CLI

**El clúster HPRC del GICC desde tu terminal.** `hprc` hace, sin salir de la consola, lo
mismo que la web del laboratorio: iniciar sesión con tu cuenta institucional, mirar tus
proyectos, empaquetar un experimento con sus archivos y su entorno, enviarlo a Slurm,
seguir el job en vivo y recoger los resultados.

```console
$ hprc submit . --name "resnet-baseline" --wait
```

- **Para quién**: miembros del laboratorio **GICC** de la **Universidad San Ignacio de
  Loyola** (alumnos, profesores, jefes de laboratorio y administradores) con cuenta ya
  creada en la plataforma.
- **Qué NO es**: no es un cliente de Slurm ni un sustituto de `ssh` a chicken. `hprc` no
  abre sesiones remotas, no ejecuta `sbatch` por su cuenta, no crea usuarios y no
  funciona fuera de la red del laboratorio. Todo pasa por el backend HPRC, que es quien
  decide qué se puede hacer y con qué permisos.

---

## Índice

1. [Cómo funciona](#cómo-funciona)
2. [La restricción de LAN](#la-restricción-de-lan)
3. [Requisitos e instalación](#requisitos-e-instalación)
4. [Puesta en marcha](#puesta-en-marcha)
5. [Recorrido completo de `hprc submit`](#recorrido-completo-de-hprc-submit)
6. [Enviar con un token de un solo uso](#enviar-con-un-token-de-un-solo-uso)
7. [Todos los comandos](#todos-los-comandos)
8. [El manifiesto `hprc.yaml`](#el-manifiesto-hprcyaml)
9. [Configuración, sesión y variables de entorno](#configuración-sesión-y-variables-de-entorno)
10. [Códigos de salida y uso en guiones](#códigos-de-salida-y-uso-en-guiones)
11. [Solución de problemas](#solución-de-problemas)
12. [Seguridad](#seguridad)
13. [Desarrollo y licencia](#desarrollo-y-licencia)

---

## Cómo funciona

### El camino de un envío

Cuando ejecutas `hprc submit`, tu código recorre esto:

```
  TU PORTÁTIL                       CLÚSTER GICC · LAN 172.19.1.0/24
  (dentro de la LAN del GICC)
                                  ┌─────────────────────────────────────┐
  ┌──────────────┐   ZIP + JSON   │ chicken · 172.19.1.252              │
  │ hprc submit  │ ─────────────▶ │                                     │
  └──────────────┘   HTTP sin TLS │   FastAPI :8000                     │
         ▲                        │      │   valida el JWT y extrae     │
         │                        │      │   el ZIP en el NFS           │
         │  estado, logs          │      ▼                              │
         │  y resultados          │   Taskiq  +  Redis                  │
         │                        │      │                              │
         │                        │      ▼                              │
         │                        │   sudo -u <tu_os_username> sbatch   │
         └────────────────────────┤      │                              │
                                  │      ▼                              │
                                  │   slurmctld  (Slurm)                │
                                  │      │                              │
                                  └──────┬──────────────────────────────┘
                                         │
                              ┌──────────┴──────────┐
                              ▼                     ▼
                    ┌──────────────────┐  ┌──────────────────┐
                    │ falcon           │  │ eagle            │
                    │ RTX 3080         │  │ gateway;         │
                    │ único nodo Slurm │  │ nodo Slurm caído │
                    └──────────────────┘  └──────────────────┘
```

Dos consecuencias prácticas de este camino:

- **El envío es asíncrono.** `POST /jobs/submit` responde en cuanto la tarea entra en
  Taskiq, así que el `job_id` aparece al instante pero el `slurm_job_id` tarda unos
  segundos. Un bucle del backend sondea Slurm cada 10 s y actualiza el estado.
- **El job corre como tú.** El backend ejecuta `sbatch` con `sudo -u <tu_os_username>`,
  de modo que los archivos del NFS pertenecen a tu usuario del sistema.

### El camino de la autenticación

El backend **no tiene** endpoints de login: delega en Supabase y valida el token en
**cada** petición.

```
    hprc                        Supabase Auth                   chicken:8000
 tu portátil                      (GoTrue)                     backend FastAPI
LAN del GICC                     en internet                    LAN del GICC
      │                               │                               │
      │ 1. correo + contraseña        │                               │
      │───────────────────────────────▶                               │
      │                               │                               │
      │ 2. JWT (access + refresh)     │                               │
      │◀───────────────────────────────                               │
      │                               │                               │
      │ 3. Authorization: Bearer <JWT>│                               │
      │───────────────────────────────────────────────────────────────▶
      │                               │                               │
      │                               │ 4. GET /auth/v1/user, cada vez│
      │                               │◀───────────────────────────────
      │                               │                               │
      │                               │ 5. el token vale · quién eres │
      │                               │───────────────────────────────▶
      │                               │                               │
      │ 6. proyectos, jobs, resultados│                               │
      │◀───────────────────────────────────────────────────────────────
      │                               │                               │
      ▼                               ▼                               ▼
```

Por eso el CLI necesita **las dos cosas a la vez**: estar dentro de la LAN (para llegar
a chicken) y tener salida a internet (para que tanto el login como la validación de cada
petición lleguen a `*.supabase.co`).

---

## La restricción de LAN

**chicken (`172.19.1.252`) no es un servidor público: es un PC físico en la LAN
`172.19.1.0/24` del laboratorio.** Esa dirección no existe en internet y el backend no
está publicado en ningún puerto abierto al exterior. Para hablar con él hay que tener un
camino de red hasta esa LAN: estar enchufado a ella, o llegar por una VPN de malla que
enrute hasta el backend.

Antes de cada comando que hable con el backend, el CLI comprueba cuatro cosas y aborta con
**código de salida 4** si alguna falla:

1. que el host configurado **resuelva**;
2. que **todas** sus direcciones sean privadas — si el backend resolviera a una IP
   pública, la configuración está apuntando a otro sitio y el CLI se niega a mandar tu
   token allí;
3. que se pueda **abrir un socket TCP** contra él, porque una dirección privada a la que
   nadie contesta no es una red, es un error de configuración;
4. que **tu propia dirección local** en esa conexión sea también privada.

Cuentan como privadas las de RFC1918 (`10/8`, `172.16/12`, `192.168/16`), *loopback*,
*link-local*, las ULA `fc00::/7` y el rango CGNAT `100.64.0.0/10`. Ese último no lo marca
como privado la biblioteca `ipaddress` de Python; el CLI lo añade **a propósito**, porque
es el rango que reparten las VPN de malla del estilo de Tailscale.

```console
$ hprc --api-url http://8.8.8.8:8000 dashboard
✖ HPRC CLI solo opera dentro de la LAN del clúster GICC (172.19.1.0/24) o en el propio
chicken. El backend HPRC no puede estar en una dirección pública: «8.8.8.8» resuelve a
8.8.8.8. chicken (172.19.1.252) solo es alcanzable desde la LAN del clúster GICC
(172.19.1.0/24), así que una IP global significa que la configuración apunta a otro
servidor.
  ↳ Apunta el CLI al backend de la LAN: hprc config set api_url http://172.19.1.252:8000
$ echo $?
4
```

Conviene entender bien qué frontera dibuja esto. La guardia no comprueba que estés
físicamente en el laboratorio: comprueba que el backend sea una dirección privada
alcanzable y que tú estés dentro de ese mismo espacio privado. **Si el laboratorio monta
una VPN de malla que enrute hasta el backend, un equipo autorizado en ella satisface la
guardia desde donde sea**, y eso es deliberado, no un agujero.

Lo que la guardia sigue impidiendo es lo importante: que la configuración apunte a un
backend público y el CLI mande tu token allí. Lo que cambia es quién decide el acceso. Ya
no lo decide la toma de red de la sala, sino la propia red de malla, con su autorización
de dispositivo y su lista de control de acceso; ese control es más explícito que «tener
una IP privada», pero es otro, y quien administre el laboratorio debe saber que la puerta
está ahí. Sin un camino de red hasta el backend, **ningún** comando que lo toque va a
funcionar, con o sin CLI.

Y además de la LAN hace falta **salida a internet**: la autenticación va contra Supabase,
que vive fuera. Una red del laboratorio sin salida deja el CLI igual de inservible que una
red de casa: podrás llegar a chicken, pero no podrás iniciar sesión ni renovar el token, y
el propio backend rechazará tus peticiones porque no puede validarlas.

La única comprobación que **no** aborta por estar fuera de la LAN es `hprc doctor`: te lo
cuenta como un diagnóstico más, para que puedas usarlo desde casa y ver qué falla.

---

## Requisitos e instalación

- **Python 3.11 o superior** (la integración continua lo prueba en 3.11, 3.12 y 3.13).
- Linux o macOS. En Windows, a través de WSL: fuera de él no está probado.
- Para *instalar* basta con tener internet; para *operar*, hay que estar en la LAN del
  clúster.

Clona el repositorio e instálalo como **herramienta aislada**, para que sus dependencias no
se mezclen con las de tus proyectos:

```bash
git clone https://github.com/giccai/gicc-hprc-cli.git
cd gicc-hprc-cli

pipx install .        # con pipx
uv tool install .     # o con uv, si lo prefieres
```

Cualquiera de las dos deja la orden `hprc` en tu `PATH`.

Para **desarrollar** sobre el CLI, en un entorno virtual y en modo editable (los cambios se
ven al instante, sin reinstalar):

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```

Comprueba la instalación:

```console
$ hprc --version
hprc 1.0.0
```

Autocompletado en tu intérprete de órdenes (bash, zsh, fish):

```bash
hprc --install-completion
```

---

## Puesta en marcha

Tres órdenes, una sola vez:

```bash
hprc init      # dónde está el clúster
hprc login     # quién eres
hprc doctor    # ¿está todo bien?
```

### 1. `hprc init` — configurar el CLI

Pregunta tres datos y **valida cada uno contra el servidor antes de guardar nada**:

| Dato | De dónde sale |
|---|---|
| **URL del backend HPRC (chicken)** | `http://172.19.1.252:8000`, que es el valor por defecto. Solo cámbialo si el administrador te dice otro. Se comprueba con `GET /` (tiene que responder `online`). |
| **URL del proyecto de Supabase** | La de producción del GICC, ya propuesta por defecto. |
| **Clave publicable de Supabase** (`sb_publishable_…`) | **La reparte el administrador del GICC. No está en este repositorio ni se puede deducir de él.** Se comprueba con `GET /auth/v1/settings`. |

```console
$ hprc init

╭───┬──────────────────────────────────┬──────────────────────────────────╮
│   │ Comprobación                     │ Resultado                        │
├───┼──────────────────────────────────┼──────────────────────────────────┤
│ ✔ │ Backend HPRC (GET /)             │ HPRC-Core 0.1.0 en línea         │
│ ✔ │ Supabase (GET /auth/v1/settings) │ clave aceptada por Supabase Auth │
╰───┴──────────────────────────────────┴──────────────────────────────────╯

✔ Configuración guardada en /home/jperez/.config/hprc/config.toml (permisos 0600).
    Backend HPRC  http://172.19.1.252:8000
        Supabase  https://tcabmilefbxmeuzqiykw.supabase.co
Clave publicable  sb_publishable_j0T…4zMN

▸ Siguiente paso: hprc login  ·  inicia sesión con tu cuenta institucional (@usil.pe o @usil.edu.pe).
```

Si vas a aprovisionar varios equipos con un guion, dale los tres datos por bandera y no
preguntará nada:

```bash
hprc init --api-url http://172.19.1.252:8000 \
          --supabase-url https://tcabmilefbxmeuzqiykw.supabase.co \
          --anon-key "$HPRC_SUPABASE_ANON_KEY"
```

### 2. `hprc login` — iniciar sesión

Basta el usuario: **el CLI prueba los dos dominios institucionales**. La institución usa
`@usil.pe` **y** `@usil.edu.pe` en producción, con cuentas reales en los dos, así que
escribir solo el nombre de cuenta no basta para saber cuál es el tuyo:

| Lo que escribes | Con qué se intenta entrar |
|---|---|
| `hprc login jperez` | `jperez@usil.pe` y, **solo si Supabase rechaza esas credenciales**, `jperez@usil.edu.pe`. Dos intentos como mucho. |
| `hprc login jperez@usil.edu.pe` | Ese correo y ninguno más. Un único intento. |

```console
$ hprc login jperez
? Contraseña de jperez: ********
✔ Sesión iniciada como Juan Pérez · Administrador.
▸ Backend: http://172.19.1.252:8000
▸ Siguiente paso: hprc dashboard
```

Mientras no se sepa el dominio, la contraseña se pide para **`jperez` a secas**: dar por
buena una dirección que quizá ni existe solo despistaría. Si al final entraste por el
segundo dominio, el CLI te dice con cuál, para que lo tengas a mano la próxima vez:

```console
$ hprc login jmonterof
? Contraseña de jmonterof: ********
✔ Sesión iniciada como José Montero · Alumno.
▸ Entraste como jmonterof@usil.edu.pe.
▸ Backend: http://172.19.1.252:8000
▸ Siguiente paso: hprc dashboard
```

Solo se pasa al segundo dominio cuando el fallo es de credenciales. Ante un problema de
red, un 429 por exceso de intentos o un error del servidor, el CLI se detiene y te lo
cuenta: repetir la llamada no arreglaría nada y, con el 429, empeoraría el bloqueo. Por
eso mismo, en un guion que reintente conviene escribir el correo entero.

La contraseña se pide sin eco, se envía a Supabase y se descarta: **nunca toca el disco ni
el historial del intérprete**. Lo que se guarda es la sesión (`access_token` y
`refresh_token`), y el CLI la renueva sola cuando caduca.

**Cambio obligatorio de contraseña en el primer ingreso.** Las cuentas se crean con
`must_change_password = true`; hasta que la cambies no puedes operar. `hprc login` lo
detecta y te obliga en el acto: pide la nueva contraseña dos veces (mínimo 8 caracteres,
distinta de la actual) y registra el cambio en tu perfil. Es un paso **interactivo a
propósito**, así que en un guion falla en vez de continuar a medias:

```console
$ echo "$CLAVE" | hprc login --email jperez --password-stdin
▲ Es tu primer inicio de sesión: tienes que cambiar la contraseña antes de seguir.
✖ Tu cuenta todavía usa la contraseña inicial y el cambio solo puede hacerse de forma
interactiva.
  ↳ Ejecuta «hprc login» en una terminal, sin --no-input ni --json.
```

Más adelante puedes cambiarla cuando quieras con `hprc passwd`.

Si eres **alumno**, el laboratorio exige además completar tu ficha una sola vez con
`hprc register` (código universitario, DNI, correo institucional, celular, ciclo y año de
proyección de término). El correo universitario vale en cualquiera de los dos dominios,
`@usil.pe` o `@usil.edu.pe`; si lo escribes sin dominio se completa con `@usil.pe`.

### 3. `hprc doctor` — la primera parada ante cualquier problema

Ejecuta todas las comprobaciones (no aborta en la primera que falle) y resume al final.
**Devuelve 0 solo si no hay ningún ✖**, así que sirve tal cual en un guion.

```console
$ hprc doctor
Diagnóstico del CLI HPRC ─────────────────────────────────────────────────────────────────────────────────────
╭───┬──────────────────────────────────────────┬──────────────────────────────┬──────────────────────────────╮
│   │ Comprobación                             │ Resultado                    │ Detalle                      │
├───┼──────────────────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
│ ✔ │ Versión de Python                        │ 3.11.9                       │ /usr/bin/python3.11  ·       │
│   │                                          │                              │ Linux-6.8.0-51-generic       │
│ ✔ │ Versión del CLI                          │ hprc 1.0.0                   │ gicc-hprc-cli                │
│ ✔ │ Archivo de configuración                 │ correcta                     │ /home/jperez/.config/hprc/co │
│   │                                          │                              │ nfig.toml  ·  permisos 0600  │
│ ✔ │ Resolución DNS del backend               │ 172.19.1.252 → 172.19.1.252  │ puerto 8000  ·  2 ms         │
│ ✔ │ Guardia de LAN                           │ dentro de la LAN del clúster │ IPs: 172.19.1.252  ·  IP     │
│   │                                          │                              │ local: 172.19.1.87  ·        │
│   │                                          │                              │ latencia: 1 ms               │
│ ✔ │ Backend HPRC (GET /)                     │ en línea                     │ http://172.19.1.252:8000  ·  │
│   │                                          │                              │ HPRC-Core 0.1.0  ·  31 ms    │
│ ✔ │ Supabase alcanzable                      │ clave aceptada               │ https://tcabmilefbxmeuzqiykw │
│   │                                          │                              │ .supabase.co  ·  214 ms      │
│ ✔ │ Sesión guardada                          │ válida durante 59 min 29 s   │ jperez@usil.pe  ·  caduca    │
│   │                                          │                              │ dentro de 59 min             │
│ ✔ │ Llamada autenticada (/dashboard/summary) │ el backend acepta tu token   │ 3 indicadores  ·  0 jobs en  │
│   │                                          │                              │ cola  ·  12 ms               │
╰───┴──────────────────────────────────────────┴──────────────────────────────┴──────────────────────────────╯
9 correctas  ·  0 avisos  ·  0 fallos
Todo listo para trabajar en el clúster.
```

Cuando algo va mal, lo dice y no manda tu token a ninguna parte:

```console
$ hprc doctor
Diagnóstico del CLI HPRC ─────────────────────────────────────────────────────────────────────────────────────
╭───┬──────────────────────────────────────────┬─────────────────────────────┬───────────────────────────────╮
│   │ Comprobación                             │ Resultado                   │ Detalle                       │
├───┼──────────────────────────────────────────┼─────────────────────────────┼───────────────────────────────┤
│ ✔ │ Versión de Python                        │ 3.11.9                      │ /usr/bin/python3.11  ·        │
│   │                                          │                             │ Linux-6.8.0-51-generic        │
│ ✔ │ Versión del CLI                          │ hprc 1.0.0                  │ gicc-hprc-cli                 │
│ ✔ │ Archivo de configuración                 │ correcta                    │ /home/jperez/.config/hprc/con │
│   │                                          │                             │ fig.toml  ·  permisos 0600    │
│ ✔ │ Resolución DNS del backend               │ 172.19.1.252 → 172.19.1.252 │ puerto 8000  ·  1 ms          │
│ ✖ │ Guardia de LAN                           │ fuera de la LAN del clúster │ IPs: 172.19.1.252  ·  IP      │
│   │                                          │                             │ local: —                      │
│   │                                          │                             │ No hubo respuesta en          │
│   │                                          │                             │ 172.19.1.252:8000             │
│   │                                          │                             │ [172.19.1.252] tras 5,0 s     │
│   │                                          │                             │ (timed out). O estás fuera de │
│   │                                          │                             │ la LAN del clúster GICC       │
│   │                                          │                             │ (172.19.1.0/24), o chicken    │
│   │                                          │                             │ (172.19.1.252) está apagado o │
│   │                                          │                             │ sin el backend levantado.     │
│ ✖ │ Backend HPRC (GET /)                     │ no responde                 │ El backend HPRC no respondió  │
│   │                                          │                             │ a tiempo en                   │
│   │                                          │                             │ «http://172.19.1.252:8000/».  │
│   │                                          │                             │ ¿Estás en la LAN del clúster? │
│   │                                          │                             │ Prueba: hprc doctor           │
│ ✔ │ Supabase alcanzable                      │ clave aceptada              │ https://tcabmilefbxmeuzqiykw. │
│   │                                          │                             │ supabase.co  ·  198 ms        │
│ ✔ │ Sesión guardada                          │ válida durante 54 min 11 s  │ jperez@usil.pe  ·  caduca     │
│   │                                          │                             │ dentro de 54 min              │
│ ⚠ │ Llamada autenticada (/dashboard/summary) │ omitida                     │ la guardia de LAN falló: no   │
│   │                                          │                             │ se envía tu token fuera de la │
│   │                                          │                             │ LAN del clúster               │
╰───┴──────────────────────────────────────────┴─────────────────────────────┴───────────────────────────────╯
6 correctas  ·  1 aviso  ·  2 fallos
Corrige primero los ✖ de arriba: el resto de comandos fallarán igual.
$ echo $?
1
```

---

## Recorrido completo de `hprc submit`

`hprc submit` es el comando estrella. Recorre **diez pasos** y te va contando qué hace en
cada uno; todo dato que llegue por bandera se salta, y lo que falte se pregunta.

```bash
cd ~/proyectos/resnet-baseline
hprc submit . --project "Proyecto Demo GICC" --name "resnet-baseline" \
              --description "Línea base sin aumento de datos" --wait
```

Los diez pasos, en orden: **contexto** → **proyecto** → **datos generales** →
**empaquetado** → **subida** → **configuración** → **resumen** → **commit** → **envío** →
**seguimiento**. Así se ve una corrida entera:

```console
$ hprc submit examples/proyecto-demo -p "Proyecto Demo GICC" -n resnet-baseline \
      -d "Línea base sin aumento de datos" --code-version 9f3c1ab --yes --wait
1/10 · Contexto ────────────────────────────────────────────────────────────────────────────────
  Compruebo quién eres y contra qué backend vas a trabajar.
▸ Usuario: jperez@usil.pe
▸ Backend: http://172.19.1.252:8000  (LAN del clúster GICC, ya comprobada)
▸ Sesión de staging: c3f77a8a-3d7e-45ba-a86a-ac1fe674a86b
2/10 · Proyecto ────────────────────────────────────────────────────────────────────────────────
  Solo el líder del proyecto puede crear y enviar experimentos.
▸ Proyecto tomado de la bandera --project.
▸ Proyecto: Proyecto Demo GICC  ·  aaaaaaaa-0000-4000-8000-000000000001  ·  1 experimento(s)
▸ No hay ninguna corrida activa en el proyecto: la cola está libre.
3/10 · Datos generales ─────────────────────────────────────────────────────────────────────────
  Así identificarás esta corrida en la web y en el CLI.
4/10 · Empaquetado ─────────────────────────────────────────────────────────────────────────────
  Comprimo en tu equipo lo que va al NFS del clúster, sin la basura del proyecto.

✔ 4 archivos · 22 KB sin comprimir · 9 KB en el ZIP
▸ El proyecto trae hprc.yaml: manda su contenido sobre la autodetección.
5/10 · Subida ──────────────────────────────────────────────────────────────────────────────────
  Envío la carga a la sesión de staging c3f77a8a de chicken.

✔ Subida completa: 9 KB en chicken, ya extraídos y escaneados.
6/10 · Configuración ───────────────────────────────────────────────────────────────────────────
  El worker leerá el hprc.yaml para saber qué script ejecutar y con qué entorno.
╭─ Contenido detectado en el ZIP ─────────────────────────╮
│                                                         │
│      Script de entrada  main.py                         │
│                Entorno  requirements.txt  (venv + pip)  │
│             Argumentos  --epochs 5 --seed 42            │
│  Carpeta de resultados  outputs                         │
│             Candidatos  main.py                         │
│               Archivos  4                               │
│            Directorios  0                               │
│              hprc.yaml  presente en el proyecto         │
│                                                         │
╰─────────────────────────────────────────────────────────╯
▸ Sin cambios respecto de lo detectado: el hprc.yaml que escribió el servidor ya es correcto,
así que no llamo a zip-config.
7/10 · Resumen ─────────────────────────────────────────────────────────────────────────────────
  Última revisión antes de crear nada en la base de datos.
Resumen del envío
╭───────────────────────┬─────────────────────────────────╮
│ Campo                 │ Valor                           │
├───────────────────────┼─────────────────────────────────┤
│ Proyecto              │ Proyecto Demo GICC  ·  aaaaaaaa │
│ Nombre                │ resnet-baseline                 │
│ Descripción           │ Línea base sin aumento de datos │
│ Versión de código     │ 9f3c1ab                         │
│ Script de entrada     │ main.py                         │
│ Entorno               │ requirements.txt                │
│ Argumentos            │ --epochs 5 --seed 42            │
│ Carpeta de resultados │ outputs                         │
│ Modo de subida        │ ZIP del proyecto                │
│ Archivos              │ 4                               │
│ Tamaño                │ 22 KB  (9 KB en el ZIP)         │
│ Envío a Slurm         │ sí, en cuanto se cree           │
╰───────────────────────┴─────────────────────────────────╯
8/10 · Commit ──────────────────────────────────────────────────────────────────────────────────
  Convierto la sesión de staging en un experimento CONFIGURED del proyecto.
✔ Experimento creado: 02ceec1d-c332-4b5a-9337-57dca61ef0b4
▸ Estado inicial: CONFIGURED
9/10 · Envío ───────────────────────────────────────────────────────────────────────────────────
  El backend encola la tarea en Taskiq y esta acaba en un sbatch sobre falcon.
✔ Job creado: aa358718-442f-4399-8bf2-9b54aa25cc3a  ·  estado PENDING
▸ Experimento encolado en Taskiq; sbatch se ejecutará en unos segundos.
▸ El identificador de Slurm (slurm_job_id) tarda unos segundos en aparecer: el envío pasa por
Taskiq antes de llegar a sbatch, y un bucle del backend sondea la cola cada 10 s.
10/10 · Seguimiento ────────────────────────────────────────────────────────────────────────────
  Sondeo el estado cada 5 s, dentro del límite de 60/min.

╭─ Job aa358718 ─────────────────────────────────────────────╮
│                                                            │
│            Estado  ✔ COMPLETADO                            │
│                ID  aa358718-442f-4399-8bf2-9b54aa25cc3a    │
│         Job Slurm  5152                                    │
│       Experimento  resnet-baseline  02ceec1d               │
│              Nodo  falcon                                  │
│         Partición  gpu                                     │
│               CPU  8                                       │
│               GPU  1                                       │
│            Creado  30/08/2026 15:48  (hace 14 s)           │
│            Inicio  30/08/2026 15:48  (hace 39 s)           │
│               Fin  30/08/2026 15:48  (hace unos segundos)  │
│           En cola  —                                       │
│          Duración  3 min 7 s                               │
│         CPU total  23 min 40 s                             │
│        RSS máximo  3,0 GB                                  │
│  Código de salida  0                                       │
│                                                            │
╰────────────────────────────────────────────────────────────╯
Archivos de resultados
╭───────────────┬────────┬────────────╮
│ Archivo       │ Tamaño │ Modificado │
├───────────────┼────────┼────────────┤
│ metrics.json  │   33 B │ hace 2 min │
│ resultado.txt │   45 B │ hace 2 min │
│ modelo.bin    │ 2,0 KB │ hace 2 min │
╰───────────────┴────────┴────────────╯
Cómo recoger los resultados ────────────────────────────────────────────────────────────────────
▸ Todo en un ZIP:  hprc jobs download aa358718-442f-4399-8bf2-9b54aa25cc3a --all -o .
▸ Un archivo:      hprc jobs download aa358718-442f-4399-8bf2-9b54aa25cc3a --file metrics.json -o .
▸ Informe en PDF:  hprc jobs report aa358718-442f-4399-8bf2-9b54aa25cc3a
▲ Los archivos del NFS se borran 72 h después de que el job termina.
✔ El job terminó correctamente.
```

Y después, la recogida:

```console
$ hprc jobs download aa358718 --all -o ./resultados
✔ 1 archivo(s) descargado(s) (2 KB) en resultados
```

### Detalles que conviene saber

- **Sin `--wait`**, el comando termina en el paso 9 y te da la orden exacta para seguir el
  job por tu cuenta (`hprc jobs status <job_id> --watch`). Con `--wait`, el CLI sondea
  hasta que el job termina; añade `--follow-logs` para ver además la salida en vivo.
- **Qué se sube.** Por defecto se comprime el directorio entero **en tu equipo**,
  descartando `__pycache__`, `.git`, `venv`, `node_modules`, `.idea`, `.vscode`, `*.pyc`,
  `.DS_Store` y compañía. Límites: 2 GB comprimido y 10 GB descomprimido. Para un conjunto
  de datos grande, súbelo aparte: `--file datos.csv --file-type DATASET`. Si ya tienes un
  ZIP hecho, `--zip mi-proyecto.zip`.
- **Solo el líder del proyecto** puede crear y enviar experimentos. El paso 2 te avisa
  antes de subir nada si no lo eres, y también si el proyecto ya tiene una corrida activa
  (que provocaría un 409 al enviar).
- **Nada queda a medias.** Si cancelas con Ctrl-C o algo falla después de haber subido
  archivos, el CLI borra la sesión de staging en el servidor para no dejar basura en el
  NFS.
- **`--no-submit`** registra el experimento en estado `CONFIGURED` sin encolarlo en Slurm:
  útil para revisar qué detectó el servidor antes de ocupar la GPU. Eso sí, un experimento
  así solo se puede lanzar después desde la web del GICC: el CLI encola en el mismo paso en
  que crea el experimento.

---

## Enviar con un token de un solo uso

A veces hay que mandar un experimento desde una máquina donde no toca escribir la
contraseña: el PC compartido del laboratorio, la sesión de otra persona, un guion de una
asignatura, una terminal prestada. Para eso está el **token de envío**.

Es una credencial que **emite el administrador del GICC** y te entrega. Actúa **en nombre
de una persona concreta**, queda atada a **un proyecto concreto** y vale para **un
experimento y solo uno**: en cuanto el job entra en la cola de Slurm, el token se agota.

Lo que autoriza es exactamente la cadena de `hprc submit` y nada más —subir los archivos,
crear el experimento, encolarlo— más leer el estado, el log y la lista de resultados de
*ese* job y la ficha de *su* proyecto. Todo lo demás (listar proyectos, borrar
experimentos, ver usuarios, emitir otro token) responde `403`.

> **El token sustituye a la contraseña, no al perímetro de red.** Sigue haciendo falta
> estar dentro de la LAN del clúster (`172.19.1.0/24`) y tener el CLI configurado con
> `hprc init`. Lo único que te ahorra es el `hprc login`.

Y **no eleva permisos: los acota.** Hereda los de la persona en cuyo nombre se emitió, así
que si esa persona no es líder (*owner*) del proyecto, el `commit` fallará con `403` igual
que si hubiera entrado con su contraseña.

### Qué es y cuánto dura

- **30 caracteres** del alfabeto `A-Z a-z 0-9` **sin los ambiguos `0 O 1 l I`**, para que se
  pueda dictar por teléfono sin equivocarse.
- **El servidor solo guarda su SHA-256.** El token se ve **una única vez**, en el panel de
  `hprc token create`. Si se pierde no se recupera: se revoca y se emite otro.
- **Nunca se escribe en disco.** En este modo no hay `session.json`: el token vive en la
  variable de entorno o en la bandera, viaja en la cabecera `Authorization` y desaparece al
  terminar el proceso.
- **Nunca se imprime entero** fuera de ese panel. En mensajes, tablas, salida `--json` y
  modo `--verbose` aparece enmascarado: `Kd9m…8e`.

| Estado | Cuándo llega | Qué se puede hacer con él |
|---|---|---|
| `EMITIDO` | al crearlo | el envío completo: subir, crear el experimento y encolarlo |
| `AGOTADO` | al encolar el job | solo mirar estado, log y lista de resultados de ese job |
| `CADUCADO` | a las **48 h** por defecto (`--hours`, de 1 a 168) | nada |
| `REVOCADO` | cuando el administrador ejecuta `hprc token revoke` | nada |

Los tres últimos responden `401` con el motivo exacto —«caducó», «ya fue usado», «fue
revocado»— para que sepas que hay que **pedir otro**, no que has escrito algo mal.

### Parte 1 — el administrador lo emite

```console
$ hprc token create --user jperez@usil.pe --project 11111111-1111-4111-8111-111111111111

┏━ Token de envío — cópialo AHORA ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃                                                                                       ┃
┃                            Kd9mPq2XvT7hLbNc4RwZ6yFj3sGa8e                             ┃
┃                                                                                       ┃
┃                 No se volverá a mostrar: el servidor solo guarda su hash.             ┃
┃                          Si se pierde, revócalo y emite otro.                         ┃
┃                                                                                       ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
Identificador  77777777-7777-4777-8777-777777777777
    Actúa por  Juana Pérez (jperez@usil.pe)
     Proyecto  Deteccion de anomalias  (11111111)
        Vence  dentro de 1 días · 01/09/2026 07:00 (48 h de vigencia)

╭─ Qué debe ejecutar quien lo reciba ───────────────────────────────────────────────────╮
│                                                                                       │
│  Desde el directorio de su proyecto, en la LAN del clúster:                           │
│    hprc submit . --token Kd9mPq2XvT7hLbNc4RwZ6yFj3sGa8e                               │
│                                                                                       │
│  Mejor aún, para que no quede en el historial del intérprete:                         │
│    export HPRC_TOKEN=Kd9mPq2XvT7hLbNc4RwZ6yFj3sGa8e                                   │
│    hprc submit .                                                                      │
│                                                                                       │
╰───────────────────────────────────────────────────────────────────────────────────────╯
Necesita «hprc init» hecho y estar dentro de la LAN del clúster (172.19.1.0/24). El token
se agota al enviar el experimento.
```

Junto al token hay que pasarle a la persona el **UUID del proyecto** (sale en la ficha, y
también con `hprc projects list`): lo necesita para configurarlo en su equipo.

`hprc token create` solo lo puede ejecutar el **administrador**; a cualquier otra cuenta el
backend le responde `403`.

### Parte 2 — la persona lo usa

En su equipo, dentro de la LAN y con `hprc init` ya hecho:

```bash
# 1. El proyecto sale del token, pero el CLI necesita su UUID para la primera llamada.
hprc config set default_project 11111111-1111-4111-8111-111111111111

# 2. El token, en una variable, para que no quede en el historial del intérprete.
export HPRC_TOKEN=Kd9mPq2XvT7hLbNc4RwZ6yFj3sGa8e

# 3. Y a enviar. Es el mismo `hprc submit` de siempre.
cd ~/proyectos/resnet-baseline
hprc submit . --name "resnet-baseline" --yes
```

El asistente recorre sus diez pasos igual que con sesión, con tres diferencias visibles:

```console
[1/10] Contexto: Compruebo quién eres y contra qué backend vas a trabajar.
▸ Credencial: token de un solo uso Kd9m…8e · sin hprc login y sin session.json.
…
[9/10] Envío: El backend encola la tarea en Taskiq y esta acaba en un sbatch sobre falcon.
✔ Job creado: 44444444-4444-4444-8444-444444444444  ·  estado PENDING
▲ El token Kd9m…8e quedó agotado: ya envió su experimento y no sirve para otra corrida.
```

Detalles que conviene tener claros:

- **`--project` se rechaza junto a `--token`.** El proyecto lo fija el token, y un token
  solo vale para el suyo. Por eso el UUID va en `default_project` (o en `HPRC_DEFAULT_PROJECT`):
  un token no puede listar proyectos para buscarlo por nombre, así que hace falta el UUID
  completo. Si el configurado no es el del token, el CLI se detiene en el paso 2 —con un
  `403` del backend— **antes de empaquetar ni subir un solo byte**.
- **La bandera manda sobre la variable**: si están las dos, se usa `--token`.
- **`hprc login` no hace falta y `session.json` no se toca.** El CLI tampoco habla con
  Supabase: no hay ninguna sesión que validar ni que renovar.
- **Descargar los resultados sí necesita tu propia sesión** (o la web del GICC): de las
  rutas de resultados, el token solo tiene autorizada la que los *lista*. Recuerda que los
  archivos del NFS se borran **72 h** después de que el job termine.
- **Con `--no-submit` el token se queda a medias**: crea el experimento pero no lo encola, y
  como el CLI solo encola en el mismo paso en que crea, ese experimento habrá que lanzarlo
  desde la web. El asistente lo avisa.

### Administrar los tokens emitidos

```console
$ hprc token list
Tokens de envío
╭──────────┬──────────────────────────────┬────────────────────────┬─────────┬───────────┬──────────────────┬─────────────╮
│ ID       │ Actúa por                    │ Proyecto               │ Estado  │ Emitido   │ Vence            │ Experimento │
├──────────┼──────────────────────────────┼────────────────────────┼─────────┼───────────┼──────────────────┼─────────────┤
│ 77777777 │ Juana Pérez (jperez@usil.pe) │ Deteccion de anomalias │ EMITIDO │ hace 15 h │ dentro de 1 días │ —           │
│ 88888888 │ Juana Pérez (jperez@usil.pe) │ Deteccion de anomalias │ AGOTADO │ hace 15 h │ dentro de 1 días │ 33333333    │
╰──────────┴──────────────────────────────┴────────────────────────┴─────────┴───────────┴──────────────────┴─────────────╯
2 token(s) · el token en claro no se guarda en ninguna parte: solo se ve al emitirlo
```

`hprc token list` **nunca** muestra el token, ni siquiera enmascarado: el backend no lo
envía y la tabla no tiene columna para él. Filtra con `--status emitido|agotado|revocado|caducado`.

Para anular uno antes de tiempo —se filtró, se emitió por error, la persona ya no lo
necesita—:

```console
$ hprc token revoke 77777777 --yes
✔ Token 77777777-7777-4777-8777-777777777777 revocado: ya no sirve para nada.
```

Es irreversible e idempotente, y acepta el identificador abreviado que muestra la tabla.

---

## Todos los comandos

Recórrelos con `hprc --help` y `hprc <grupo> --help`; cada uno acepta `--help`.

### Configuración y cuenta

| Comando | Qué hace |
|---|---|
| `hprc init` | Asistente de configuración inicial: pregunta backend, Supabase y clave publicable, los valida contra el servidor y los guarda. |
| `hprc config show` | Muestra la configuración efectiva y de dónde sale cada valor (entorno, archivo o defecto). `--path` imprime solo la ruta; la clave se enmascara siempre. |
| `hprc config set <clave> <valor>` | Cambia una clave (`api_url`, `supabase_url`, `anon_key`, `default_project`, `color`) conservando el resto. |
| `hprc config path` | Imprime la ruta del archivo de configuración. |
| `hprc login [usuario]` | Inicia sesión y guarda la sesión localmente. Con el usuario a secas prueba los dos dominios institucionales (`@usil.pe` y `@usil.edu.pe`); con el correo entero, solo ese. |
| `hprc logout` | Cierra la sesión en Supabase y borra las credenciales guardadas. |
| `hprc whoami` | Ficha del usuario: nombre, correo, rol, usuario del sistema y caducidad del token. |
| `hprc passwd` | Cambia tu contraseña de Supabase. |
| `hprc register` | Completa la ficha de alumno exigida por el laboratorio (solo para el rol `alumno`). |
| `hprc doctor` | Diagnostica la instalación: configuración, DNS, LAN, backend, Supabase, sesión y una llamada autenticada real. |
| `hprc version` | Versión del CLI, del intérprete y configuración efectiva (sin la clave). |

### Proyectos

| Comando | Qué hace |
|---|---|
| `hprc projects list` | Lista los proyectos visibles para tu cuenta. `--status`, `--limit`, `--offset`, `--mine`. |
| `hprc projects show <proyecto>` | Ficha de un proyecto: datos, miembros y últimos experimentos. Acepta UUID, prefijo del UUID o nombre. |
| `hprc projects use [proyecto]` | Fija (o borra, con `--clear`) el proyecto por defecto de los demás comandos. |
| `hprc projects members <proyecto>` | Miembros de un proyecto, con su rol y su antigüedad. |
| `hprc projects create` | Crea un proyecto nuevo; quedas como líder (*owner*). |

### Experimentos

| Comando | Qué hace |
|---|---|
| `hprc submit [RUTA]` | Empaqueta un proyecto, crea el experimento y lo envía al clúster. El comando estrella. Con `--token` (o `HPRC_TOKEN`) envía **sin `hprc login`**: ver [Enviar con un token de un solo uso](#enviar-con-un-token-de-un-solo-uso). |
| `hprc experiments list` | Lista los experimentos de un proyecto (`--project`, o el proyecto por defecto). |
| `hprc experiments show <experimento>` | Ficha de un experimento: datos, archivos e historial de jobs. |
| `hprc experiments delete <experimento>` | Borra un experimento y sus archivos. Pide confirmación salvo con `--yes`. |

### Trabajos (jobs)

| Comando | Qué hace |
|---|---|
| `hprc jobs list` | Tus trabajos: la cola personal y las corridas ya terminadas. |
| `hprc jobs status <job_id>` | Ficha completa de un job: estado, nodo, partición, CPU/GPU, tiempos, RSS máximo y código de salida. `--watch` refresca hasta que termine. |
| `hprc jobs watch <job_id>` | Sigue un job en vivo hasta que termine (atajo de `status --watch`). |
| `hprc jobs logs <job_id>` | Vuelca el stdout y el stderr. `-f` sigue el log en vivo; `--stdout-only` / `--stderr-only` filtran. |
| `hprc jobs results <job_id>` | Lista los archivos de resultados con tamaño y fecha. |
| `hprc jobs download <job_id>` | Descarga resultados: `--all` (un ZIP), `--file NOMBRE` (repetible), `-o DIR`, `--force`. |
| `hprc jobs report <job_id>` | Descarga en PDF el reporte del job. |

### Tokens de envío (solo administrador)

Credenciales de un solo uso para que alguien mande **un** experimento sin escribir su
contraseña. El detalle, en [Enviar con un token de un solo uso](#enviar-con-un-token-de-un-solo-uso).

| Comando | Qué hace |
|---|---|
| `hprc token create --user <correo> --project <proyecto>` | Emite un token a nombre de esa persona y atado a ese proyecto. Lo muestra **una sola vez**, con la orden exacta que debe ejecutar quien lo reciba. `--hours N` cambia la vigencia (1-168; por defecto 48). |
| `hprc token list` | Tokens emitidos con su estado (`EMITIDO`, `AGOTADO`, `REVOCADO`, `CADUCADO`), su persona, su proyecto y sus fechas. **Nunca muestra el token.** `--status`, `--limit`, `--offset`. |
| `hprc token revoke <id>` | Anula un token de inmediato y para siempre. Acepta el identificador abreviado de la tabla; pide confirmación salvo con `--yes`. |

### Panorama

| Comando | Qué hace |
|---|---|
| `hprc dashboard` | Panel de inicio: indicadores, tu cola de trabajos y resultados recientes. `--watch` lo refresca cada 10 s. |
| `hprc cluster` | Estado del clúster: nodos, GPU y jobs activos. **Solo para el administrador**; si Prometheus está apagado lo dice sin fallar. |

### Opciones globales

Van **antes** del comando (`hprc --json dashboard`) y muchas también se aceptan después:

| Opción | Para qué |
|---|---|
| `-V`, `--version` | Versión del CLI. |
| `--no-color` | Sin color ni estilos (equivale a `NO_COLOR=1`). |
| `--no-input` | Nunca preguntes nada: si falta un dato, falla indicando qué bandera lo aporta. |
| `--json` | JSON puro por la salida estándar, sin adornos. |
| `--api-url URL` | Backend HPRC solo para esta ejecución. |
| `--config RUTA` | Archivo de configuración alternativo. |
| `-v`, `--verbose` | Traza cada petición HTTP en `stderr`. |
| `--install-completion` | Instala el autocompletado en tu intérprete de órdenes. |

---

## El manifiesto `hprc.yaml`

Colócalo en la **raíz** de tu proyecto, con ese nombre exacto (`hprc.yml` no vale). Si
existe, **manda sobre la autodetección del servidor**; si no, el servidor autodetecta lo
que falte y escribe él un `hprc.yaml` equivalente dentro del proyecto, para que quede
constancia de con qué se ejecutó.

```yaml
entry: main.py            # script de entrada, relativo a la raíz del proyecto
env: requirements.txt     # environment.yml → conda; requirements.txt → venv + pip
args: "--epochs 50"       # argumentos extra para el script, en UNA sola cadena
outputs: outputs          # carpeta de resultados
```

Son las **cuatro únicas claves** que el servidor interpreta; cualquier otra se ignora.
Todas son opcionales salvo `entry`, que es lo único imprescindible para ejecutar.

| Clave | Qué es | Autodetección si la omites |
|---|---|---|
| `entry` | Script que ejecuta el worker en falcon (`.py`, `.sh`, `.r`/`.R`, `.m`). | `main.py` → `experiment.py` → `run.py` → `train.py`; si no hay ninguno, el primer archivo **de la raíz** con extensión ejecutable, en orden alfabético. |
| `env` | Archivo que describe el entorno. | `environment.yml` → `environment.yaml` → `requirements.txt`. Si no hay ninguno, se usa el Python del sistema (funciona, pero no es reproducible). |
| `args` | Argumentos que recibe el script, tal cual. | Ninguno. La bandera `--args` de `hprc submit` tiene prioridad sobre esta clave. |
| `outputs` | Carpeta donde tu script escribe. El worker la sustituye por un enlace simbólico al directorio de resultados del job. | La primera de `outputs`, `output`, `results`, `result` que exista. |

Dos reglas que ahorran disgustos:

1. **La autodetección de `entry` solo mira la raíz.** Si tu script vive en `src/`,
   decláralo (`entry: src/train.py`) o el servidor no lo encontrará.
2. **Lo que escribas fuera de `outputs` no se recoge.** Se pierde al limpiar el directorio
   de trabajo. Si esa carpeta ya traía contenido en el ZIP, no se mezcla: se reubica como
   `_reference_outputs/`.

La plantilla comentada completa está en
[`examples/hprc.yaml.example`](examples/hprc.yaml.example), y hay un proyecto de ejemplo
que funciona de verdad en [`examples/proyecto-demo/`](examples/proyecto-demo/).

---

## Configuración, sesión y variables de entorno

### Dónde vive cada cosa

| Archivo | Ruta (Linux) | Ruta (macOS) | Contenido | Permisos |
|---|---|---|---|---|
| Configuración | `~/.config/hprc/config.toml` | `~/Library/Application Support/hprc/config.toml` | URL del backend, URL de Supabase, clave publicable, proyecto por defecto y color. | `0600` (archivo), `0700` (directorio) |
| Sesión | `~/.local/share/hprc/session.json` | `~/Library/Application Support/hprc/session.json` | `access_token`, `refresh_token`, caducidad, tu id y tu correo. | `0600` (archivo), `0700` (directorio) |

Las dos rutas salen de `platformdirs`, así que respetan `XDG_CONFIG_HOME` y
`XDG_DATA_HOME` donde apliquen. `hprc config path` te dice la primera; **la contraseña no
se guarda en ninguna de las dos**.

El `config.toml` tiene cinco claves y se puede editar a mano (conservando el `0600`):

```toml
api_url = "http://172.19.1.252:8000"
supabase_url = "https://tcabmilefbxmeuzqiykw.supabase.co"
anon_key = "sb_publishable_…"
default_project = "aaaaaaaa-0000-4000-8000-000000000001"
color = true
```

### Variables de entorno

Mandan sobre el archivo. La precedencia completa es
**variables de entorno > archivo TOML > valor por defecto**, y `hprc config show` te dice
de dónde salió cada valor:

| Variable | Para qué |
|---|---|
| `HPRC_CONFIG` | Ruta alternativa del `config.toml` (equivale a `--config`). |
| `HPRC_API_URL` | URL del backend HPRC (equivale a `--api-url`). |
| `HPRC_SUPABASE_URL` | URL del proyecto de Supabase. |
| `HPRC_SUPABASE_ANON_KEY` | Clave publicable de Supabase. Útil para no dejarla en disco en un equipo compartido. |
| `HPRC_DEFAULT_PROJECT` | UUID del proyecto por defecto. |
| `HPRC_TOKEN` | Token de envío de un solo uso para `hprc submit` (equivale a `--token`, que manda sobre ella). Es la forma recomendada de pasarlo: así no queda en el historial del intérprete. **No se guarda en disco.** |
| `NO_COLOR` | Definida y no vacía, desactiva el color (ver <https://no-color.org>). |

```console
$ hprc config show
╭─────────────────┬──────────────────────────────────────────┬─────────╮
│ Clave           │ Valor                                    │  Origen │
├─────────────────┼──────────────────────────────────────────┼─────────┤
│ api_url         │ http://172.19.1.252:8000                 │ archivo │
│ supabase_url    │ https://tcabmilefbxmeuzqiykw.supabase.co │ archivo │
│ anon_key        │ sb_publishable_j0T…4zMN                  │ archivo │
│ default_project │ —                                        │ defecto │
│ color           │ sí                                       │ archivo │
╰─────────────────┴──────────────────────────────────────────┴─────────╯
▸ Archivo: /home/jperez/.config/hprc/config.toml  ·  permisos 0600
```

---

## Códigos de salida y uso en guiones

Todos los comandos devuelven uno de estos siete códigos. Están pensados para que un guion
del laboratorio pueda decidir si reintentar, avisar o abortar:

| Código | Significado | Qué suele querer decir |
|:---:|---|---|
| `0` | Éxito | Todo fue bien. |
| `1` | Error genérico | Fallo del backend (403, 404, 429…), error de red o cancelación del usuario con Ctrl-C. |
| `2` | Uso incorrecto | Bandera o argumento mal escrito; lo emite Typer/Click. |
| `3` | No autenticado | No hay sesión, o caducó y no se pudo renovar. Ejecuta `hprc login`. |
| `4` | Fuera de la LAN | El equipo no está en la red del clúster, o el backend configurado no es privado. |
| `5` | Conflicto de negocio | El backend respondió 409: ya hay un experimento activo, el experimento ya se envió, etc. |
| `6` | El job terminó en `FAILED` | Solo con `hprc submit --wait`: el envío fue bien, pero el trabajo falló en el nodo. |

```bash
#!/usr/bin/env bash
set -uo pipefail

hprc doctor >/dev/null || { echo "Entorno mal configurado"; exit 1; }

hprc submit . --yes --wait --json -n "barrido-$(date +%F)" > salida.json
case $? in
  0) echo "Job COMPLETED: $(jq -r .job.id salida.json)" ;;
  5) echo "Ya hay una corrida activa en el proyecto; reintento más tarde" ;;
  6) echo "El job falló: $(jq -r .job.id salida.json)"; exit 6 ;;
  *) echo "Error al enviar"; exit 1 ;;
esac
```

Para guiones, dos banderas globales:

- **`--json`** emite un único objeto JSON por `stdout`, sin adornos; el relato de los pasos
  se va a `stderr`. En `hprc submit`, el objeto `job` solo viene relleno si pediste
  `--wait` (sin él, el CLI no llega a consultar el estado del job).
- **`--no-input`** prohíbe cualquier pregunta: si falta un dato, el comando falla diciendo
  qué bandera lo aporta en vez de quedarse esperando. El CLI lo da por supuesto por su
  cuenta cuando `stdin` o `stdout` no son una terminal, así que en un `cron` no se cuelga
  nunca, con bandera o sin ella.

---

## Solución de problemas

Ante cualquier duda, **empieza siempre por `hprc doctor`**: comprueba en orden
configuración, DNS, LAN, backend, Supabase, sesión y una llamada autenticada real, y te
dice cuál de esas es la que falla.

| Síntoma | Qué pasa | Qué hacer |
|---|---|---|
| **«Correo o contraseña incorrectos»** en `hprc login` (salida `3`) | Supabase rechazó las credenciales. Si escribiste el usuario a secas, el mensaje enumera los correos probados («Se probó con `jperez@usil.pe` y `jperez@usil.edu.pe`»): ninguno de los dos dominios coló. | Revisa la contraseña. Si sabes cuál de los dos dominios es el tuyo, entra con el correo entero (`hprc login jperez@usil.edu.pe`): es un intento en lugar de dos. Si la contraseña se perdió, pide al administrador del GICC que la reinicie. |
| **401** «token expirado / no autenticado» (salida `3`) | La sesión caducó y el refresco tampoco funcionó. El CLI ya intenta renovar el token una vez por su cuenta. | `hprc login`. Si se repite muy a menudo, revisa la salida a internet: el token se renueva contra Supabase. |
| **403** «solo el líder del proyecto» (salida `1`) | Solo el *owner* puede hacer staging, commit y submit. Ser miembro no basta. | `hprc projects list --mine` te dice cuáles lideras. Para los demás, pide al líder que envíe él, o que te transfiera el proyecto. |
| **409** «ya hay un experimento activo en este proyecto» (salida `5`) | **falcon tiene una sola GPU**, así que el backend admite un único experimento activo (`PENDING`/`RUNNING`) por proyecto. | Espera a que termine el actual (`hprc dashboard` o `hprc jobs list`) o cancélalo. `hprc submit` ya te avisa de esto en el paso 2, antes de subir nada. |
| **409** «el experimento ya fue enviado» (salida `5`) | Un experimento admite **un solo** job: tras enviarlo queda sellado. | Para otra corrida, vuelve a ejecutar `hprc submit`: se crea un experimento nuevo. |
| **429** «Rate limit exceeded» (salida `1`) | Se superó el límite de peticiones de esa ruta. El envío de jobs es el más estricto: **5 por minuto**. Consultar estado, 60/min. | El mensaje incluye cuántos segundos esperar. Espera y reintenta; en un bucle, sube el intervalo de sondeo. |
| **Fuera de la LAN** (salida `4`) | El equipo no está en `172.19.1.0/24`, chicken está apagado, o la URL configurada no apunta a una dirección privada. | Conéctate a la red del laboratorio. Comprueba con `ping 172.19.1.252` y `hprc doctor`. Si la URL está mal: `hprc config set api_url http://172.19.1.252:8000`. |
| **Falta la clave publicable** | No hay `config.toml`, o no tiene `anon_key`. | `hprc init`. La clave la reparte el administrador del GICC. |
| **`hprc jobs results` no devuelve nada** | Mientras el job no esté `COMPLETED`, la lista de resultados viene vacía. | Espera a que termine (`hprc jobs watch <job_id>`) y vuelve a pedirla. |
| **El job termina en `FAILED`** (salida `6` con `--wait`) | Casi siempre es la instalación del entorno o una excepción del script. | `hprc jobs logs <job_id>`. Prueba el proyecto en local antes de enviarlo: si falla ahí, fallará en falcon. |
| **`hprc cluster` dice que no hay métricas** | Prometheus no está levantado (el backend devuelve 503), o no eres administrador (403). | No es un fallo del CLI. Solo el rol `administrador` ve el estado del clúster. |

### Retención: **72 horas**

> **Los archivos de resultados se borran del NFS 72 horas después de que el job termina.**

Pasado ese plazo queda únicamente la metadata (el experimento aparece con
`files_purged_at` relleno) y ya no hay nada que descargar. Recoge lo que te importe a
tiempo:

```bash
hprc jobs download <JOB_ID> --all -o ./resultados
hprc jobs report   <JOB_ID>
```

---

## Seguridad

- **En este repositorio no hay ningún secreto**, y no debe haberlo nunca. La clave
  publicable de Supabase (`sb_publishable_…`) **la reparte el administrador del GICC**; el
  CLI la pide en `hprc init` y la guarda solo en tu equipo. La clave de servicio de
  Supabase no sale jamás del servidor y el CLI no la usa para nada: si `hprc init` detecta
  que has pegado algo que no empieza por `sb_publishable_`, te avisa.
- **La contraseña nunca toca el disco.** Se pide sin eco (o por `--password-stdin`), se
  envía a Supabase y se descarta. No aparece en logs, ni en mensajes de error, ni en el
  historial del intérprete.
- **Qué se guarda y con qué permisos**: `config.toml` (clave publicable y URLs) y
  `session.json` (`access_token` y `refresh_token`), ambos con permisos **`0600`** en un
  directorio **`0700`**, y escritos de forma atómica. Con `hprc logout` la sesión se cierra
  en Supabase y el archivo se borra.
- **El token de envío tampoco toca el disco.** Llega por `--token` o por `HPRC_TOKEN`, viaja
  en la cabecera `Authorization` y muere con el proceso: en ese modo no se lee ni se escribe
  `session.json`. Se ve entero **una única vez**, al emitirlo; en cualquier otro sitio —tablas,
  errores, `--json` y `--verbose`— sale enmascarado (`Kd9m…8e`). El servidor solo guarda su
  SHA-256, así que ni el administrador puede recuperarlo: si se pierde, se revoca y se emite otro.
- **`hprc config show` enmascara la clave** (se ven los 18 primeros caracteres y los 4
  últimos), lo justo para reconocerla sin exponerla en una captura de pantalla o al pedir
  ayuda.
- **El CLI no envía tu token fuera de la LAN.** Si la guardia de red falla, aborta antes de
  hacer la petición; `hprc doctor` marca esa comprobación como «omitida» en lugar de
  intentarla.
- `.gitignore` ya excluye `config.toml`, `session.json` y `.env*`, para que un descuido no
  acabe en un commit.

---

## Desarrollo y licencia

¿Vas a tocar el código? Empieza por [`CONTRIBUTING.md`](CONTRIBUTING.md): montar el
entorno, `ruff check`, `ruff format`, `pytest` y las convenciones del proyecto. El
historial de versiones está en [`CHANGELOG.md`](CHANGELOG.md).

```bash
pip install -e ".[dev]"
ruff check . && ruff format --check . && pytest
```

Licencia **MIT** — ver [`LICENSE`](LICENSE).

---

<div align="center">

**GICC · Universidad San Ignacio de Loyola**
chicken · eagle · falcon

</div>
