Puesta en marcha de un nodo

Clonar styx y sus SDK hermanos en un árbol fijo, comprobar la toolchain exacta y compilar contra clones locales, con un solo comando por nodo.

Implementado. scripts/dev/bootstrap.sh, scripts/dev/setup.ts y el manifiesto scripts/dev/workspace.yml están en el repositorio y se han ejecutado de principio a fin en un nodo cloud Linux x86_64: dos corridas seguidas (la segunda sin cambios), verify en verde y un --fork local compilando el daemon. En macOS y en el servidor Linux no se han ejecutado todavía: las notas de esos nodos son lo que el script hace en ellos, no evidencia.

Styx se compila y se prueba en varias máquinas. Para no clonar a mano cada repo ni adivinar qué versión de cada herramienta hace falta, cada nodo se prepara igual: un árbol de trabajo fijo, un manifiesto que dice qué va en él y un doctor que compara lo instalado con lo que fija el repo.

El comando

git clone https://github.com/MKS2508/styx.git ~/src/styx-ws/styx && sh ~/src/styx-ws/styx/scripts/dev/bootstrap.sh

Para otro sitio, exporta STYX_WORKSPACE antes (por ejemplo, un SSD externo). Clona master; STYX_REF elige otra rama.

Después, en cada shell:

. ~/src/styx-ws/env.sh
bun ~/src/styx-ws/styx/scripts/dev/setup.ts verify

El árbol

$STYX_WORKSPACE            (por defecto ~/src/styx-ws)
├── env.sh                 STYX_WORKSPACE y, si hace falta, el bun fijado en el PATH
├── .tools/bun-<versión>/  sólo si el bun del PATH no es el de .bun-version
├── styx/                  el monorepo (y su submódulo docs/references/quic-zig)
├── sdk/
│   ├── zkit/              master — primitivas Zig del daemon (dec-0103)
│   ├── spire/             main   — spire-zig + @spire/bus (dec-0120)
│   ├── conduit/           master — ingesta: IngestSink y styx-upload (dec-0121)
│   └── quic-zig/          main   — fork QUIC; el build usa el submódulo, no este clon (dec-0105)
├── tools/axon/            opcional — el CLI ya llega como @mks2508/axon
└── apps/mks-iptv-app/     opcional — app Swift de la fusión (sólo macOS)

Lo declara scripts/dev/workspace.yml: id, URL https, ruta, rama por defecto (comprobada con git ls-remote --symref), papel, si es opcional, y qué dependencia Zig o paquete npm publica. Un repo nuevo se añade ahí y no en el script. Los opcionales se clonan con clone --all o clone --only axon. Un repo inaccesible (privado sin credenciales, borrado) sale como FALTA con el error de git y los demás siguen.

Qué hace bootstrap.sh

Es sh POSIX y no necesita bun:

  1. Detecta Linux o macOS y x86_64 o arm64/aarch64. Otro SO o arquitectura es un error explícito.
  2. Encuentra styx: el checkout que lo contiene o, ejecutado suelto, clona styx en $STYX_WORKSPACE/styx.
  3. Exige el bun exacto de .bun-version. Si el del PATH es otro, descarga ese release de GitHub, comprueba su sha256 contra SHASUMS256.txt y lo deja en .tools/. No toca el bun global. En Linux x86_64 sin AVX2 elige la variante baseline y con musl la musl.
  4. Escribe env.sh sólo si cambia.
  5. Llama a setup.ts clone, deps y doctor. Con argumentos (sh bootstrap.sh doctor --libav) ejecuta sólo eso.

Es idempotente: no vuelve a descargar ni a clonar, y no reescribe nada. Un clon que ya existe no se toca, aunque tenga cambios o esté en otra rama; sólo se informa.

setup.ts

SubcomandoQué hace
doctor [--libav] [--json]Compara lo instalado con lo que fija el repo. Sale con 1 si algo está en FALTA
clone [--all] [--only a,b]Crea el árbol del manifiesto
depsInicializa docs/references/quic-zig si no lo está (si está en otro commit, avisa y lo deja), ejecuta bun install --frozen-lockfile y, con el Zig fijado, zig build --fetch
infra up|down|status [--dry-run]Postgres, Valkey, NATS y OTel de deploy/ con el override de dev (puertos sólo en 127.0.0.1). up crea antes los secretos si faltan, también con --dry-run; status falla si aún no hay deploy/.env
zig [--fork a,b] [-- args]zig build en native/zig contra clones locales de los SDK
link spire / unlink spire@spire/bus del clon local en lugar del pin publicado, y la vuelta
verify [--fork a,b]Ejecuta zig build, bun run typecheck y una prueba de humo: styx-upload sin argumentos sale con su mensaje de uso
workspaceImprime la raíz resuelta

deps, infra, zig, link y verify actúan sobre $STYX_WORKSPACE/styx. Con --repo actúan sobre otro checkout.

SDK privados sin credenciales de git

@spire/bus (dependencia github: del lockfile) y conduit/spire de native/zig/build.zig.zon viven en repos privados, y ni bun install ni zig build --fetch usan las credenciales de git: un nodo sin acceso recibe un 404. Con SDK_READ_TOKEN exportado (PAT fine-grained con sólo contents:read sobre MKS2508/spire y MKS2508/conduit), deps instala con scripts/ci/with-sdk-token.sh y precarga en la caché de Zig, verificadas por su .hash, las dependencias de GitHub de build.zig.zon con scripts/ci/zig-fetch-private-deps.sh: lo mismo que hace CI con el secreto de Actions del mismo nombre. El token no se escribe en ningún fichero.

export SDK_READ_TOKEN=<pat>   # sólo en la sesión; nunca en env.sh ni en el repo
sh scripts/dev/bootstrap.sh

Qué comprueba doctor

Ninguna versión esperada está copiada en el script. Todas se leen del checkout:

ComprobaciónContra qué
bun.bun-version (en ramas que no lo tienen, packageManager). Tiene que ser exacto
zigminimum_zig_version de native/zig/build.zig.zon, exacto. Avisa si no coincide con el suelo de build.zig. El arreglo trae el tarball de tu SO y arquitectura y, en x86_64-linux, el sha256 fijado en build.zig
git, git-lfsgit siempre. git-lfs sólo si .gitattributes usa filter=lfs (hoy no lo usa: N/A)
quic-zigEl submódulo está inicializado y en el commit fijado
node_modulesHay bun install hecho
docker/podmanCliente, daemon que responde y docker compose v2. Con sólo podman sale un aviso: los compose no se han probado con él
nodePlaywright corre con node, no con bun (tools/e2e-harness/*/run.sh)
chromiumLa revisión que pide el @playwright/test del harness, en PLAYWRIGHT_BROWSERS_PATH o la caché por defecto del SO
libavSólo con --libav: nasm, compilador C, make y FFmpeg 8 construido en native/zig/libav-bridge/.build/prefix
puertosCada puerto de deploy/docker-compose.infra.dev.yml, con sus STYX_DEV_*_PORT. Si lo tiene un contenedor deploy-*, la infra ya está arriba y es OK. Si lo tiene otro proceso, falta
discoPor debajo de 5 GiB libres es FALTA. Por debajo de 10 GiB o por encima del 85 % de uso, AVISO

Cada FALTA y cada AVISO traen el arreglo para el SO en el que se ejecuta. Ejemplo real del nodo cloud:

OK    bun            bun 1.4.2
OK    zig            zig 0.17.0
OK    quic-zig       submódulo en 07101255
OK    puerto 5433    STYX_DEV_PG_PORT ya lo publica deploy-postgres-1 (la infra de dev está arriba)
AVISO disco        …: 8.2 GiB libres, 78% usado (aviso por debajo de 10 GiB o por encima del 85%)
                     → limpia ~/.cache/zig, .zig-cache/ y zig-out/ viejos
16 OK · 0 FALTA · 2 AVISO · 2 N/A

Compilar contra clones locales de los SDK

build.zig.zon fija zkit, spire y conduit por URL y hash. Para probar un cambio en un SDK sin tocar el build.zig.zon, Zig 0.17 tiene zig build --fork=<ruta>. Sustituye en todo el grafo cada paquete que tenga el mismo nombre que el build.zig.zon de esa ruta:

bun scripts/dev/setup.ts zig --fork spire            # = zig build --fork=$STYX_WORKSPACE/sdk/spire
bun scripts/dev/setup.ts zig --fork spire,conduit -- test:container
bun scripts/dev/setup.ts verify --fork conduit

Zig lo dice en cada build (info: fork …/sdk/spire matched 1 spire packages). Sin --fork se vuelve al pin publicado: no hay nada que deshacer. Para comprobar que el fork compila de verdad el clon y no el pin, se añadió un @compileError al root.zig del clon de spire: el build con --fork spire falló en esa línea y el build sin --fork siguió en verde.

Tres límites, comprobados con el Zig fijado:

  • Si el clon no es compatible, el build falla. Para eso sirve el fork. Al escribir esta página, conduit master ya iba por delante del pin con un cambio de API (server.Completed sin sha256). Con el clon en el commit del pin, el build es verde.
  • --fork zkit falla mientras dos SDK fijen zkit distintos. zkit entra dos veces en el grafo: la del daemon (que conduit comparte) y otra de spire. El fork sustituye las dos por la misma ruta y Zig responde file exists in modules zkit-… and zkit-…. Antes hay que alinear el pin de zkit en spire. setup.ts lo recuerda cuando se pide --fork zkit.
  • quic-zig no admite fork. Es una dependencia .path (el submódulo) y --fork sólo sustituye paquetes por URL (matched no quic packages). Se trabaja dentro de styx/docs/references/quic-zig, y sdk/quic-zig queda para ramas y PR al fork.

@spire/bus local

Los servicios fijan @spire/bus como github:MKS2508/spire#<commit> en sus package.json. Con el linker aislado de bun, todos los workspaces enlazan una única copia en node_modules/.bun/@spire+bus@…/node_modules/@spire/bus. bun link sólo cambiaría el enlace de la raíz. Por eso:

bun scripts/dev/setup.ts link spire     # esa copia pasa a ser un enlace a sdk/spire/packages/bus
bun scripts/dev/setup.ts unlink spire   # vuelve el pin publicado

link ejecuta bun install --frozen-lockfile en el clon de spire, si hace falta, para que sus dependencias se resuelvan desde allí, y guarda la copia publicada como ….pinned. No cambia ningún package.json ni el bun.lock. bun install no deshace el enlace: lo da por bueno (no changes). La vuelta es unlink. Se probó así: link, @spire/bus resuelto al clon desde apps/catalog-svc, el typecheck y los tests de packages/bus contra el clon, y después unlink, la resolución de vuelta al store y git status limpio.

La pila en local

setup.ts deja la toolchain y la infraestructura; los servicios no tienen un dev que arranque solo, porque cada uno exige su configuración (base de datos, identidad del bus, OIDC, socket del daemon: la lista está en la cabecera de su src/bootstrap.ts). Por orden:

bun scripts/dev/setup.ts infra up                  # Postgres :5433, Valkey :6380, NATS :4223, OTLP :14317/:14318 en 127.0.0.1
NATS_URL=nats://localhost:4223 bun run bus:streams # streams del bus, con la identidad de despliegue

Para los servicios hay tres caminos, de más a menos completo:

  • Los harness de tools/e2e-harness/: web-vod/run.sh levanta daemon, catalog, playback, identity y la web con identidades del bus efímeras y su propio nats-server con la ACL, y run-gate.ts levanta los servicios como contenedores. Su cabecera dice qué necesitan.
  • Los compose de deploy/ con el override de dev: docker compose -f deploy/docker-compose.infra.yml -f deploy/docker-compose.infra.dev.yml -f deploy/docker-compose.apps.yml -f deploy/docker-compose.apps.dev.yml up -d --build. docker-compose.apps.dev.yml monta tests/fixtures/ por una ruta absoluta de la máquina en la que se escribió: hay que cambiarla en otra.
  • Un servicio suelto con bun --watch src/bootstrap.ts desde su carpeta, exportando a mano las variables de su cabecera. Las semillas del bus están en deploy/secrets/ y el keyring en deploy/.env tras deploy:secrets; el NATS de dev escucha en el 4223, no en el 4222 por defecto.

Cada servicio sirve su OpenAPI en /openapi/json y la interfaz Scalar en /openapi, sin CDN. Lo controla STYX_OPENAPI=off|json|ui: ui por defecto fuera de producción, off en producción, y si se activa en producción exige rol de administrador.

La web, sin mks-dev-session: bun run --cwd apps/web dev:vite. El sitio de documentación: bun run --cwd apps/docs dev.

Por tipo de nodo

Nodo cloud efímero (Linux x86_64, contenedor)

  • El contenedor no persiste. Se ejecuta bootstrap.sh al empezar cada sesión. Es idempotente, y la caché de bun y la de Zig, si sobreviven, lo hacen rápido. La primera corrida completa tardó menos de un minuto con la caché caliente. verify, que incluye compilar el daemon, unos siete.
  • Salida HTTPS por proxy con CA propia. El entorno ya exporta las variables de CA (SSL_CERT_FILE, NODE_EXTRA_CA_CERTS); doctor dice si están. El manifiesto usa URLs https porque el proxy reescribe las de ssh. Un repo que el proxy no deja pasar sale como FALTA en clone, sin colgarse pidiendo credenciales.
  • El bun de la imagen puede no ser el fijado. bootstrap instala el exacto en .tools/ y env.sh lo antepone en el PATH.
  • Se ejecuta como root. bun run deploy:secrets, que infra up lanza si faltan secretos, se niega a dejar las semillas del operador a root. Exporta STYX_OPERATOR_UID con un uid no root.
  • Puede haber una infra arriba de otro checkout. Todos los checkouts comparten el proyecto de compose deploy (el nombre del directorio), así que infra up desde otro checkout recrea esos contenedores con sus propios secretos. infra up --dry-run enseña qué haría Compose, pero si faltan deploy/secrets/ o deploy/.env los genera de verdad antes (no es un simulacro de deploy:secrets). doctor muestra quién tiene cada puerto.
  • Disco justo. Mira la línea de disco de doctor y borra el workspace al acabar si era temporal.
  • Chromium de Playwright suele venir en la imagen (PLAYWRIGHT_BROWSERS_PATH).

Mac de desarrollo (Apple Silicon, SSD externo)

  • STYX_WORKSPACE=/Volumes/<disco>/styx-ws antes de bootstrap.sh, y en el perfil del shell. El volumen tiene que ser APFS: con exFAT no hay enlaces simbólicos ni permisos POSIX, y el linker aislado de bun, el env.sh y el enlace de @spire/bus los necesitan.
  • Zig: el tarball zig-aarch64-macos-<versión>.tar.xz de la versión fijada (0.17.0, de ziglang.org/download/0.17.0/). brew sólo vale si da exactamente esa versión. build.zig fija el sha256 de x86_64-linux, aarch64-linux, x86_64-macos y aarch64-macos (scripts/pin-zig.ts, que antes verifica la firma .minisig), y doctor imprime el comando con la comprobación.
  • Contenedores: OrbStack o Docker Desktop. Los puertos de dev ya evitan el 5432 de un Postgres del sistema (el de dev es el 5433).
  • libav (--libav): brew install nasm make coreutils. build-ffmpeg.sh usa nproc, que en macOS viene de coreutils.
  • Playwright: playwright install chromium, sin --with-deps, que es sólo para Linux.
  • apps/mks-iptv-app (opcional) sólo tiene sentido aquí, con Xcode.

Servidor Linux (Hetzner)

  • Debian o Ubuntu: sudo apt-get install -y git curl unzip xz-utils, Docker desde el repositorio oficial (docker-ce y docker-compose-plugin) y tu usuario en el grupo docker. Con libav, además nasm build-essential pkg-config.
  • Trabaja con un usuario normal. deploy:secrets con sudo desde ese usuario reparte cada secreto al uid de su contenedor y usa SUDO_UID como operador.
  • En ARM (serie CAX), bun usa bun-linux-aarch64 y Zig zig-aarch64-linux-…. Esa plataforma tampoco tiene sha256 fijado: hay que verificar la firma.
  • El override de dev publica los puertos sólo en 127.0.0.1, así que la infra no queda expuesta a la red pública del servidor. Para llegar desde fuera, usa un túnel SSH, no 0.0.0.0.
  • Playwright: playwright install --with-deps chromium instala también las librerías del sistema.

Lo que no está verificado

  • bootstrap.sh y setup.ts sólo se han ejecutado en el nodo cloud Linux x86_64. En macOS y en el servidor no.
  • infra up real no se lanzó en esa verificación, porque había una infra de otro checkout arriba con el mismo proyecto de compose. Sólo se probaron --dry-run y la generación de secretos.
  • doctor --libav con FFmpeg construido no se probó.
  • Los tres caminos de La pila en local no se ejecutaron en la auditoría del 2026-10-02: la infraestructura de dev ya estaba arriba desde otro checkout.
  • Con sólo podman, doctor avisa pero no comprueba nada más.

Relacionado: Empezar a contribuir para el mapa del repositorio y los comandos del día a día, y Ciclo de vida para el recorrido de desarrollar a publicar.