@carlos-burelo/wyrm 1.0.0

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.
package/README.md ADDED
@@ -0,0 +1,242 @@
1
+ # Wyrm 🐉
2
+
3
+ **Wyrm** es un gestor de procesos de ultra alto rendimiento escrito en **Rust**, enfocado en **Windows Server**. Mantiene aplicaciones Node.js, Next.js, Astro y Express corriendo como Servicio de Windows nativo con Zero-Config.
4
+
5
+ - Supervisor real con `JobObjects` (`KILL_ON_JOB_CLOSE`), auto-restart con backoff y restore al arrancar.
6
+ - IPC por Named Pipe con protocolo tipado `Request/Response` NDJSON.
7
+ - CLI completa: `start/stop/restart/delete/list/status/logs/daemon/top/service`.
8
+ - TUI de primer nivel con Ratatui: lista, métricas CPU/MEM, logs en vivo, filtro y acciones.
9
+ - Logs por app en `%ProgramData%\wyrm\logs\<name>.log`.
10
+ - DB SQLite en `%ProgramData%\wyrm\wyrm.db`.
11
+
12
+ ## Instalación
13
+
14
+ ```powershell
15
+ npm install -g @carlos-burelo/wyrm
16
+ ```
17
+
18
+ O desde fuente:
19
+
20
+ ```powershell
21
+ cargo build --release
22
+ ```
23
+
24
+ ## Quickstart
25
+
26
+ ```powershell
27
+ # En tu proyecto Node (auto-detecta package.json, .next/standalone, Astro, pnpm/yarn/bun)
28
+ wyrm start
29
+
30
+ # Ver estado (funciona con o sin demonio)
31
+ wyrm list
32
+
33
+ # Demonio en foreground (dev)
34
+ wyrm daemon
35
+
36
+ # TUI interactiva
37
+ wyrm top
38
+ # o simplemente:
39
+ wyrm
40
+ ```
41
+
42
+ Con demonio corriendo:
43
+
44
+ ```powershell
45
+ wyrm stop mi-app
46
+ wyrm restart mi-app
47
+ wyrm status mi-app
48
+ wyrm logs mi-app --lines 100
49
+ wyrm logs mi-app --follow
50
+ wyrm delete mi-app --yes
51
+ ```
52
+
53
+ Multi-app con ecosystem file:
54
+
55
+ ```powershell
56
+ wyrm init # genera wyrm.json del proyecto actual
57
+ wyrm start --all # levanta todas las apps (wyrm.json o ecosystem.json)
58
+ wyrm start --all --file ./prod.json
59
+ ```
60
+
61
+ `wyrm.json`:
62
+
63
+ ```json
64
+ { "apps": [{ "name": "web", "cwd": ".", "env": { "PORT": "3000" } }] }
65
+ ```
66
+
67
+ `executable`/`args` opcionales: si faltan se auto-detectan. `cwd` relativo
68
+ al archivo. El `env` del archivo gana sobre `.env`.
69
+
70
+ Políticas por app (`policy: {...}` en `wyrm.json` o payload `START`):
71
+
72
+ ```json
73
+ {
74
+ "policy": {
75
+ "max_restarts": 10,
76
+ "min_uptime_secs": 5,
77
+ "stop_timeout_secs": 5,
78
+ "max_memory_mb": 512,
79
+ "healthcheck_url": "http://127.0.0.1:3000/health",
80
+ "healthcheck_secs": 30
81
+ }
82
+ }
83
+ ```
84
+
85
+ - Salir antes de `min_uptime_secs` suma crash-loop; al llegar a
86
+ `max_restarts` la app queda `ERRORED` (requiere `wyrm restart` manual).
87
+ - `max_memory_mb` reinicia al superar la RSS; `healthcheck_url` con 3 fallos
88
+ seguidos mata el proceso para que el supervisor lo reinicie.
89
+ - Logs rotan a 10 MiB conservando 5 históricos (`app.log.1…5`).
90
+ - No se persisten en SQLite (solo vive en ecosystem/`START`; el restore usa
91
+ defaults). En Windows el stop es terminate (CTRL+BREAK elegante: futuro).
92
+
93
+ Diagnóstico:
94
+
95
+ ```powershell
96
+ wyrm doctor # node, demonio, servicio, DB, disco, logs + hints
97
+ ```
98
+
99
+ ## Deploy (git + hooks)
100
+
101
+ ```powershell
102
+ wyrm deploy <app> [--ref main] # pre hook → git sync → post hook → restart
103
+ wyrm releases <app> [--limit 10]
104
+ wyrm rollback <app> # git reset al último deploy ok + restart
105
+ ```
106
+
107
+ Hooks en `wyrm.json` por app: `pre_deploy` / `post_deploy` (shell `cmd /C`).
108
+ Historial en SQLite (`deploys`).
109
+
110
+ ## API local + métricas (plano de control)
111
+
112
+ El demonio sirve `http://127.0.0.1:8379` (`WYRM_API_ADDR`, off con
113
+ `WYRM_NO_API=1`). Auth Bearer con token en `%ProgramData%\wyrm\token`:
114
+
115
+ ```powershell
116
+ wyrm token # muestra el token
117
+ wyrm token --rotate
118
+ curl -H "Authorization: Bearer $t" http://127.0.0.1:8379/apps
119
+ curl -H "Authorization: Bearer $t" http://127.0.0.1:8379/metrics # Prometheus
120
+ ```
121
+
122
+ Endpoints: `GET /health` (público), `GET /apps`, `GET /apps/:name`,
123
+ `POST /apps/:name/{start,stop,restart}`, `GET /metrics`
124
+ (`wyrm_app_up/uptime/restarts/cpu/memory`).
125
+
126
+ ## Edge + TLS (mini-PAAS)
127
+
128
+ ```powershell
129
+ wyrm route add <host> <http://127.0.0.1:3000>
130
+ wyrm route list / wyrm route rm <host>
131
+ wyrm edge # :80 (WYRM_EDGE_PORT) + :443 SNI (WYRM_EDGE_TLS_PORT, 0=off)
132
+ wyrm cert issue <host> [--staging|--prod] [--email x@y] # requiere edge en :80
133
+ wyrm cert selfsigned <host> # solo dev
134
+ wyrm cert list / wyrm cert renew [--force]
135
+ ```
136
+
137
+ `wyrm.json` acepta `routes: [{host, target}]` (se aplican con `start --all`).
138
+ TLS con SNI + wildcard, recarga de certs cada 60s. Proxy HTTP bufferizado
139
+ (16 MB, sin websockets todavía).
140
+
141
+ Servicio Windows (producción):
142
+
143
+ ```powershell
144
+ wyrm service install
145
+ wyrm service uninstall
146
+ ```
147
+
148
+ ## TUI (`wyrm top`) — mejor que `pm2 monit`
149
+
150
+ Dashboard dos paneles: lista + detalle + sparklines CPU/MEM + preview logs.
151
+ Tabs `1/2/3` o `tab` para Dashboard / Logs / Help.
152
+
153
+ | Tecla | Acción |
154
+ |---|---|
155
+ | `1/2/3`, `tab` | cambiar tab |
156
+ | `j/k` o `↑/↓` | navegar apps o scroll logs |
157
+ | `enter` / `l` | ver logs full de la app |
158
+ | `r` | restart |
159
+ | `s` / `S` | stop / start (revive STOPPED desde DB, sin cwd) |
160
+ | `o` | ciclo orden: nombre → cpu → mem → uptime → restarts |
161
+ | `d` luego `y` | delete con confirmación |
162
+ | `/` | filtrar apps o buscar en logs |
163
+ | `f` / `G` | follow on/off / ir al final en logs |
164
+ | `F` luego `y` | vaciar log actual |
165
+ | `?` | ayuda |
166
+ | `q` / `esc` | salir o volver |
167
+
168
+ Logs con colores por nivel (error rojo, warn amarillo, ok/ready verde) y
169
+ resaltado de búsqueda. Header con demonio, CPU global, MEM y conteo running.
170
+ Detalle con pid, executable, cwd, log path y gauge de MEM del sistema.
171
+
172
+ ## Arquitectura
173
+
174
+ ```
175
+ CLI (clap) ──Named Pipe NDJSON──> Daemon
176
+ │ ├─ HashMap<String, ManagedApp>
177
+ │ ├─ JobObject por proceso
178
+ │ ├─ logs a archivo (append)
179
+ │ └─ supervise loop cada 2s + backoff
180
+ └─ fallback DB (SQLite) si demonio off
181
+
182
+ crates/wyrm/src:
183
+ main.rs bootstrap (16 líneas)
184
+ api/ HTTP 127.0.0.1:8379 + token + /metrics Prometheus
185
+ cli/ args + start/manage/list/status/logs/init/deploy/token/route/cert/doctor
186
+ ecosystem/ wyrm.json multi-app (+routes, hooks, policy)
187
+ deploy/ hooks cmd.exe + git sync/reset
188
+ daemon/ state + supervisor + health loop
189
+ edge/ proxy host→target + acme HTTP-01 + tls SNI
190
+ ipc/ pipe + protocol Request/Response
191
+ store/ SQLite (apps, deploys, routes) + paths
192
+ runtime/ inspector/policy/process/service Windows
193
+ logs/ read/tail/flush/rotate compartido cli+tui
194
+ tui/ state/data/actions/events/theme/views
195
+ ```
196
+
197
+ ## Límites conocidos (honestos)
198
+
199
+ - **ACME real sin probar E2E**: `cert issue --prod` requiere DNS público
200
+ apuntando al servidor + `wyrm edge` en :80. Usa `--staging` primero.
201
+ - **Proxy HTTP bufferizado**: sin websockets, cuerpo máximo 16 MB.
202
+ - **Expiración de certs estimada**: `meta.json` asume 90 días (no parse X.509).
203
+ - **Stop en Windows = terminate**: `stop_timeout_secs` es ventana de espera;
204
+ CTRL+BREAK elegante queda futuro.
205
+ - **Policies fuera de SQLite**: viven en `wyrm.json`/payload `START`;
206
+ el restore tras reinicio usa defaults.
207
+ - **API solo loopback** (`127.0.0.1:8379`); el token vive en archivo con los
208
+ permisos del FS del SO.
209
+ - **Single-node**: un demonio por pipe, sin clustering multi-máquina.
210
+ - **Primer release solo x64 Windows** (sin binario ARM64).
211
+ - **Healthcheck TLS** con roots embebidos (sin CAs corporativas custom).
212
+
213
+ ## Release (mantenedores)
214
+
215
+ Todo automático al pushear un tag `vX.Y.Z`:
216
+
217
+ ```powershell
218
+ git tag v0.1.0
219
+ git push origin v0.1.0
220
+ ```
221
+
222
+ CI hace: `cargo test` → build x64 release → GitHub Release con
223
+ `wyrm-x86_64-pc-windows-msvc.exe` + `.sha256` → sync de versión en
224
+ `npm/package.json` → `npm publish`. El `postinstall` descarga el asset de
225
+ tu misma versión y verifica SHA256.
226
+
227
+ Requisitos: secret `NPM_TOKEN` en el repo. Verificación local previa:
228
+
229
+ ```powershell
230
+ cargo test -p wyrm
231
+ node --check npm/scripts/install.js
232
+ npm pack --dry-run # en npm/: confirma 4 archivos, sin vendor/
233
+ ```
234
+
235
+ ## Desarrollo
236
+
237
+ ```powershell
238
+ cargo check -p wyrm
239
+ cargo test -p wyrm
240
+ cargo fmt
241
+ cargo clippy -p wyrm
242
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const { spawn } = require('child_process');
5
+ const path = require('path');
6
+
7
+ const binPath = path.join(__dirname, '..', 'vendor', 'wyrm.exe');
8
+
9
+ if (!fs.existsSync(binPath)) {
10
+ console.error(
11
+ '[wyrm] Falta vendor/wyrm.exe. Reinstala (npm i -g @carlos-burelo/wyrm) o compila local con `npm run build`.'
12
+ );
13
+ process.exit(1);
14
+ }
15
+
16
+ const child = spawn(binPath, process.argv.slice(2), {
17
+ stdio: 'inherit',
18
+ windowsHide: true,
19
+ });
20
+
21
+ child.on('error', (err) => {
22
+ console.error(`[wyrm] No se pudo lanzar el binario: ${err.message}`);
23
+ process.exit(1);
24
+ });
25
+
26
+ child.on('exit', (code) => {
27
+ process.exit(code || 0);
28
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@carlos-burelo/wyrm",
3
+ "version": "1.0.0",
4
+ "description": "El gestor de procesos definitivo para Windows Server escrito en Rust",
5
+ "main": "bin/cli.js",
6
+ "files": [
7
+ "bin/",
8
+ "scripts/",
9
+ "README.md"
10
+ ],
11
+ "bin": {
12
+ "wyrm": "bin/cli.js"
13
+ },
14
+ "scripts": {
15
+ "postinstall": "node scripts/install.js"
16
+ },
17
+ "os": [
18
+ "win32"
19
+ ],
20
+ "cpu": [
21
+ "x64"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/carlos-burelo/wyrm.git"
26
+ },
27
+ "keywords": [
28
+ "wyrm",
29
+ "pm2",
30
+ "windows-server",
31
+ "rust",
32
+ "process-manager",
33
+ "nextjs",
34
+ "astro"
35
+ ],
36
+ "author": "Carlos Burelo",
37
+ "license": "MIT"
38
+ }
@@ -0,0 +1,123 @@
1
+ // Postinstall de wyrm: deja vendor/wyrm.exe listo.
2
+ // - Versión desde package.json (la sincroniza el workflow de release).
3
+ // - Salta si ya hay binario local (npm run build) o WYRM_SKIP_DOWNLOAD.
4
+ // - Verifica SHA256 del asset .sha256 cuando existe.
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const https = require('https');
8
+ const crypto = require('crypto');
9
+
10
+ const REPO = 'carlos-burelo/wyrm';
11
+ const BIN_DIR = path.join(__dirname, '..', 'vendor');
12
+ const BIN_NAME = 'wyrm.exe';
13
+ const BIN_PATH = path.join(BIN_DIR, BIN_NAME);
14
+
15
+ const TARGETS = {
16
+ 'win32-x64': 'x86_64-pc-windows-msvc',
17
+ };
18
+
19
+ function pkgVersion() {
20
+ const { version } = require('../package.json');
21
+ if (!version) throw new Error('package.json sin version');
22
+ return String(version).replace(/^v/, '');
23
+ }
24
+
25
+ function targetTriple(platform = process.platform, arch = process.arch) {
26
+ const triple = TARGETS[`${platform}-${arch}`];
27
+ if (!triple) {
28
+ throw new Error(
29
+ `Plataforma no soportada: ${platform}-${arch} (wyrm v1 solo publica win32-x64)`
30
+ );
31
+ }
32
+ return triple;
33
+ }
34
+
35
+ function assetName(triple) {
36
+ return `wyrm-${triple}.exe`;
37
+ }
38
+
39
+ function download(url, dest, redirects = 5) {
40
+ return new Promise((resolve, reject) => {
41
+ https
42
+ .get(url, (res) => {
43
+ const { statusCode, headers } = res;
44
+ if ((statusCode === 301 || statusCode === 302) && headers.location) {
45
+ if (redirects <= 0) return reject(new Error('demasiadas redirecciones'));
46
+ res.resume();
47
+ return resolve(download(headers.location, dest, redirects - 1));
48
+ }
49
+ if (statusCode !== 200) {
50
+ res.resume();
51
+ return reject(new Error(`HTTP ${statusCode} en ${url}`));
52
+ }
53
+ const file = fs.createWriteStream(dest);
54
+ res.pipe(file);
55
+ file.on('finish', () => file.close(resolve));
56
+ file.on('error', reject);
57
+ })
58
+ .on('error', reject);
59
+ });
60
+ }
61
+
62
+ function sha256File(p) {
63
+ return new Promise((resolve, reject) => {
64
+ const h = crypto.createHash('sha256');
65
+ fs.createReadStream(p)
66
+ .on('data', (d) => h.update(d))
67
+ .on('end', () => resolve(h.digest('hex')))
68
+ .on('error', reject);
69
+ });
70
+ }
71
+
72
+ async function install() {
73
+ if (process.env.WYRM_SKIP_DOWNLOAD) {
74
+ console.log('[wyrm] WYRM_SKIP_DOWNLOAD=1: salto la descarga.');
75
+ return;
76
+ }
77
+ if (fs.existsSync(BIN_PATH)) {
78
+ console.log('[wyrm] Binario local existente, salto la descarga.');
79
+ return;
80
+ }
81
+ const version = pkgVersion();
82
+ const triple = targetTriple();
83
+ const base = `https://github.com/${REPO}/releases/download/v${version}`;
84
+ const asset = assetName(triple);
85
+ fs.mkdirSync(BIN_DIR, { recursive: true });
86
+
87
+ console.log(`[wyrm] Descargando ${asset} v${version}...`);
88
+ try {
89
+ await download(`${base}/${asset}`, BIN_PATH);
90
+ } catch (e) {
91
+ try {
92
+ fs.unlinkSync(BIN_PATH);
93
+ } catch (_) {}
94
+ throw new Error(
95
+ `No se pudo descargar el binario (${e.message}). ¿Existe el release v${version}?`
96
+ );
97
+ }
98
+
99
+ try {
100
+ await download(`${base}/${asset}.sha256`, `${BIN_PATH}.sha256`);
101
+ const expected = fs.readFileSync(`${BIN_PATH}.sha256`, 'utf8').trim().toLowerCase();
102
+ const actual = await sha256File(BIN_PATH);
103
+ if (expected !== actual) {
104
+ fs.unlinkSync(BIN_PATH);
105
+ throw new Error('SHA256 no coincide: binario corrupto o manipulado.');
106
+ }
107
+ fs.unlinkSync(`${BIN_PATH}.sha256`);
108
+ console.log('[wyrm] SHA256 verificado.');
109
+ } catch (e) {
110
+ if (/corrupto|manipulado/.test(e.message)) throw e;
111
+ console.warn(`[wyrm] Aviso: sin checksum (${e.message}), continúo sin verificar.`);
112
+ }
113
+ console.log('[wyrm] Instalación nativa completada.');
114
+ }
115
+
116
+ if (require.main === module) {
117
+ install().catch((e) => {
118
+ console.error(`[wyrm] ${e.message}`);
119
+ process.exit(1);
120
+ });
121
+ }
122
+
123
+ module.exports = { pkgVersion, targetTriple, assetName, REPO, BIN_PATH };