ratacode 0.2.5
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/CREDITS.md +80 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/apreton/README.md +54 -0
- package/apreton/handshake.md +33 -0
- package/apreton/headless.md +58 -0
- package/apreton/mcp.md +221 -0
- package/apreton/navegador.md +58 -0
- package/bin/instalacion.js +148 -0
- package/bin/ratacode.js +1251 -0
- package/fabrica/settings.yaml +247 -0
- package/mcp/README.md +253 -0
- package/mcp/bin/ratacode-mcp.js +215 -0
- package/mcp/lib/actividad.js +49 -0
- package/mcp/lib/casa.js +126 -0
- package/mcp/lib/claves.js +148 -0
- package/mcp/lib/espacios.js +117 -0
- package/mcp/lib/http.js +184 -0
- package/mcp/lib/lectura.js +210 -0
- package/mcp/lib/modelos.js +235 -0
- package/mcp/lib/nucleo.js +332 -0
- package/mcp/lib/registro.js +21 -0
- package/mcp/lib/seguridad.js +283 -0
- package/mcp/lib/servidor.js +397 -0
- package/mcp/lib/tareas.js +406 -0
- package/mcp/package.json +22 -0
- package/mcp/tunel.mjs +209 -0
- package/modos/arquitecto/agent.cordis.yml +120 -0
- package/modos/arquitecto/preset.yml +3 -0
- package/modos/capataz/agent.cordis.yml +120 -0
- package/modos/capataz/preset.yml +3 -0
- package/modos/faro/agent.cordis.yml +120 -0
- package/modos/faro/preset.yml +3 -0
- package/modos/gepeto/agent.cordis.yml +191 -0
- package/modos/gepeto/preset.yml +3 -0
- package/modos/hero/agent.cordis.yml +198 -0
- package/modos/hero/preset.yml +3 -0
- package/modos/modo-rata/agent.cordis.yml +198 -0
- package/modos/modo-rata/preset.yml +3 -0
- package/modos/nex/agent.cordis.yml +213 -0
- package/modos/nex/preset.yml +3 -0
- package/modos/nex/skills/cordis-plugin-development/SKILL.md +420 -0
- package/modos/nex/skills/editing-cordis-compositions/SKILL.md +165 -0
- package/modos/pix/agent.cordis.yml +207 -0
- package/modos/pix/preset.yml +3 -0
- package/modos/tirita/agent.cordis.yml +130 -0
- package/modos/tirita/preset.yml +3 -0
- package/package.json +49 -0
- package/piel/activos/ratacode-emblema.svg +14 -0
- package/piel/activos/ratacode-es.js +1331 -0
- package/piel/activos/ratacode-identidad.css +50 -0
- package/piel/activos/ratacode-piel.css +217 -0
- package/piel/activos/ratacode-piel.js +487 -0
- package/piel/activos/ratacode-vida.js +424 -0
- package/piel/cordis.patch.yml +6 -0
- package/piel/lib/cliente.js +1020 -0
- package/piel/lib/index.js +1230 -0
- package/piel/package.json +36 -0
package/bin/ratacode.js
ADDED
|
@@ -0,0 +1,1251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ratacode — LA TERMINAL DE TRABAJO BRUTO.
|
|
4
|
+
*
|
|
5
|
+
* Arranca `dsh` (el motor de DSH, MIT) con una casa PROPIA
|
|
6
|
+
* (`%USERPROFILE%\.ratacode`, nunca `~/.dsh`) y con la cara de RATACODE puesta
|
|
7
|
+
* por el plugin `ratacode-piel`. Puerto fijo y conocido: 3777.
|
|
8
|
+
*
|
|
9
|
+
* Lo que hace, en orden:
|
|
10
|
+
* 0 · PUERTA: si Node es menor que 24, PARA con un mensaje claro (no arranca).
|
|
11
|
+
* 1 · Prepara la casa: `profiles/web` (bundles `dsh-base`, `dsh-web-app` y
|
|
12
|
+
* `ratacode-piel`, copiado dentro del perfil, que es como el cargador de
|
|
13
|
+
* DSH resuelve un plugin fuera del árbol) y `profiles/headless`
|
|
14
|
+
* (`dsh-base` + `dsh-headless`, el modo sin pantalla que DSH ya trae).
|
|
15
|
+
* 2 · ESTRENA la casa (sólo la primera vez, si no hay `settings.yaml`):
|
|
16
|
+
* copia `fabrica\settings.yaml` — B.AI y OpenRouter de fábrica, el aviso
|
|
17
|
+
* «Internal Testing» ya aceptado y `permission.defaultPreset:
|
|
18
|
+
* danger-full-access` (sin «Permitir» en cada paso) — copia los nueve
|
|
19
|
+
* modos de `modos\` a `<casa>/.agent-presets`, y pone el modelo por
|
|
20
|
+
* defecto del PRIMER PROVEEDOR CON CLAVE en el entorno (orden: B.AI,
|
|
21
|
+
* OpenRouter, DeepSeek). Si la casa YA existe, no se pisa nada del
|
|
22
|
+
* usuario.
|
|
23
|
+
* 3 · ESPACIO DE TRABAJO: la carpeta desde la que se lanza `ratacode` (o la
|
|
24
|
+
* de `--carpeta <ruta>`) queda REGISTRADA en `<casa>\storages\workspace.json`
|
|
25
|
+
* — el formato del registro de espacios de DSH — y es el `cwd` del motor.
|
|
26
|
+
* Además se fija el selector de carpeta EN LA WEB: se apaga la fila
|
|
27
|
+
* adaptativa de DSH (`dsh-host-directory-picker-auto`, que en Windows
|
|
28
|
+
* resuelve «native» y abre un diálogo del sistema que nadie que maneje
|
|
29
|
+
* RATACODE desde fuera puede ver) y se compone el par «browse».
|
|
30
|
+
* 4 · Arranca el motor, coge la URL CON TOKEN que imprime y la guarda en
|
|
31
|
+
* `<casa>/url.txt` SÓLO cuando el puerto ya escucha de verdad (al
|
|
32
|
+
* empezar borra la de la vez anterior, para que nadie se lleve una URL
|
|
33
|
+
* muerta). Y deja en `<casa>\ratacode.log` por qué se cierra el motor
|
|
34
|
+
* (código de salida y señal): una instancia que se cae sola tiene que
|
|
35
|
+
* dejar rastro.
|
|
36
|
+
*
|
|
37
|
+
* Y `ratacode headless "encargo" [--modelo X]`: el MISMO motor, la MISMA casa,
|
|
38
|
+
* el perfil `headless` de DSH, sin pantalla ni puerto. `--modelo` vale sólo
|
|
39
|
+
* para ese encargo: se le pasa al motor un documento de ajustes copia con el
|
|
40
|
+
* modelo cambiado, así NO se toca el que el usuario tenga guardado.
|
|
41
|
+
*
|
|
42
|
+
* Ninguna clave viaja dentro: las pone el usuario (web Ajustes > Models →
|
|
43
|
+
* `<casa>/.credentials.yaml`, o exportando la variable de entorno de cada
|
|
44
|
+
* proveedor). Los proveedores de fábrica son los 9 declarados en
|
|
45
|
+
* `fabrica\settings.yaml` (B.AI, OpenRouter, Groq, Google Gemini, NVIDIA NIM,
|
|
46
|
+
* SambaNova, Cloudflare Workers AI y DOS LOCALES sin clave: Ollama y LM Studio)
|
|
47
|
+
* más DeepSeek, que lo sirve el adaptador nativo de DSH (ruta
|
|
48
|
+
* `deepseek-official`, `https://api.deepseek.com`, `DEEPSEEK_API_KEY`): 10 en
|
|
49
|
+
* Ajustes > Models.
|
|
50
|
+
* De las claves SÓLO se mira si la variable existe: nunca su valor.
|
|
51
|
+
*
|
|
52
|
+
* El texto de la conexión y el MCP para chats web viven en Ajustes > Conexiones
|
|
53
|
+
* (la piel los sirve en `/ratacode/handshake` y `/ratacode/mcp`); para eso el
|
|
54
|
+
* plugin se lleva copiado `apreton\` y la ruta de ESTA instalación en
|
|
55
|
+
* `instalacion.txt` (para que los comandos del MCP lleven la ruta de verdad).
|
|
56
|
+
*
|
|
57
|
+
* Nada de esto toca `~/.dsh` ni el DSH de nadie más.
|
|
58
|
+
*/
|
|
59
|
+
import { spawn } from 'node:child_process';
|
|
60
|
+
import { randomUUID } from 'node:crypto';
|
|
61
|
+
import { createRequire } from 'node:module';
|
|
62
|
+
import {
|
|
63
|
+
appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync,
|
|
64
|
+
} from 'node:fs';
|
|
65
|
+
import { connect } from 'node:net';
|
|
66
|
+
import { homedir } from 'node:os';
|
|
67
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
68
|
+
import { fileURLToPath } from 'node:url';
|
|
69
|
+
import yaml from 'js-yaml';
|
|
70
|
+
|
|
71
|
+
const AQUI = dirname(fileURLToPath(import.meta.url));
|
|
72
|
+
const PAQUETE = resolve(AQUI, '..');
|
|
73
|
+
const PIEL_ORIGEN = join(PAQUETE, 'piel');
|
|
74
|
+
const NOMBRE_PLUGIN = 'ratacode-piel';
|
|
75
|
+
/** Los ajustes que se copian a una casa nueva (proveedores, aviso, permiso). */
|
|
76
|
+
const FABRICA_ORIGEN = join(PAQUETE, 'fabrica', 'settings.yaml');
|
|
77
|
+
/** Los nueve modos de fábrica, que van a `<casa>/.agent-presets`. */
|
|
78
|
+
const MODOS_ORIGEN = join(PAQUETE, 'modos');
|
|
79
|
+
/** El texto de la conexión y su guía, que viajan copiados junto al plugin de la piel. */
|
|
80
|
+
const APRETON_ORIGEN = join(PAQUETE, 'apreton');
|
|
81
|
+
|
|
82
|
+
/** Puerto fijo y conocido de RATACODE. `--port` lo cambia. */
|
|
83
|
+
const PUERTO_POR_DEFECTO = 3777;
|
|
84
|
+
/** La versión del aviso «Internal Testing» que el cliente da por aceptada. */
|
|
85
|
+
const AVISO_ACEPTADO = '2026-08-13.1';
|
|
86
|
+
/** Node mínimo: por debajo, los módulos nativos del motor no son los suyos. */
|
|
87
|
+
const NODE_MINIMO = 24;
|
|
88
|
+
/**
|
|
89
|
+
* La capa que deja SÓLO los 9 modos de RATACODE en el selector: apaga la raíz
|
|
90
|
+
* de fábrica del motor (sus 4 modos de serie no salen) y pone `modo-rata` por
|
|
91
|
+
* defecto. Va en la COMPOSICIÓN porque el namespace de ajustes del motor sólo
|
|
92
|
+
* admite `default` (`dsh-agent-presets/lib/index.js:1151`); `includeShippedRoot`
|
|
93
|
+
* no cabe en `settings.yaml` y su única puerta es esta capa (`:1246`).
|
|
94
|
+
* Va DESPUÉS de los bundles: la fila `agent-presets` la inserta `dsh-web-app`,
|
|
95
|
+
* así que un `config` por id sólo la alcanza si se aplica por encima.
|
|
96
|
+
*/
|
|
97
|
+
const CAPA_PRESETS = `# RATACODE · los 9 modos de la casa, y NINGUNO de los que trae el motor de serie.
|
|
98
|
+
- id: agent-presets
|
|
99
|
+
config:
|
|
100
|
+
default: modo-rata
|
|
101
|
+
includeShippedRoot: false
|
|
102
|
+
`;
|
|
103
|
+
/** El preset de RATACODE que una casa usa si no dice otra cosa. */
|
|
104
|
+
const PRESET_POR_DEFECTO = 'modo-rata';
|
|
105
|
+
/**
|
|
106
|
+
* El idioma de RATACODE (R21): español. Los otros dos que trae el motor
|
|
107
|
+
* (inglés y chino) siguen donde estaban: se eligen en Ajustes › General ›
|
|
108
|
+
* Language. Aquí sólo se apunta el de la casa cuando no hay ninguno apuntado.
|
|
109
|
+
*/
|
|
110
|
+
const IDIOMA_POR_DEFECTO = 'es';
|
|
111
|
+
/**
|
|
112
|
+
* Los TRES temas de RATACODE (R21), en su orden, y el de fábrica: el primero.
|
|
113
|
+
* El id tiene que ser el mismo que registra `piel\lib\cliente.js` por la vía
|
|
114
|
+
* oficial de temas de DSH.
|
|
115
|
+
*/
|
|
116
|
+
const TEMAS_DE_LA_CASA = ['ratacode-pink', 'ratacode-yellow', 'minimal'];
|
|
117
|
+
const TEMA_POR_DEFECTO = TEMAS_DE_LA_CASA[0];
|
|
118
|
+
/** Los presets que salieron en versiones anteriores de RATACODE y ya no existen. */
|
|
119
|
+
const PRESETS_VIEJOS = ['enlazador', 'promptista', 'escritor'];
|
|
120
|
+
/**
|
|
121
|
+
* Los valores de `agent-presets.default` que hay que MIGRAR a modo-rata, y sólo
|
|
122
|
+
* éstos: los que traía RATACODE de fábrica (`standard`), los que ya no existen
|
|
123
|
+
* (los tres de arriba) y los de serie del motor (`minimal`, `ptc`, `cordis`),
|
|
124
|
+
* que con `includeShippedRoot: false` no resuelven y dejan la sesión sin
|
|
125
|
+
* componer (`preset "standard" not found`, medido en R14). Cualquier otro valor
|
|
126
|
+
* —uno de los 9 modos, o un preset del usuario— NO se toca: es su elección.
|
|
127
|
+
*/
|
|
128
|
+
const PRESETS_A_MIGRAR = ['standard', 'minimal', 'ptc', 'cordis', ...PRESETS_VIEJOS];
|
|
129
|
+
const PLANTILLA_PARCHE = `# Tu capa de parches de este perfil, aplicada después de cada capa de bundle:
|
|
130
|
+
# una lista YAML de filas del cargador (config por id, desactivaciones, inserts).
|
|
131
|
+
${CAPA_PRESETS}`;
|
|
132
|
+
/** Los ajustes que DSH espera en un perfil para resolver plugins de fuera del árbol. */
|
|
133
|
+
const PLANTILLA_PNPM = `packages:
|
|
134
|
+
- .
|
|
135
|
+
|
|
136
|
+
nodeLinker: hoisted
|
|
137
|
+
autoInstallPeers: false
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* El primer proveedor con clave en el entorno, en este orden, y el modelo con
|
|
142
|
+
* el que se estrena la casa. Si el proveedor declara modelos en el
|
|
143
|
+
* `settings.yaml` de la casa, manda el PRIMERO que declare; este de aquí es
|
|
144
|
+
* sólo el respaldo cuando no declara ninguno (DeepSeek no se declara: lo sirve
|
|
145
|
+
* el adaptador nativo de DSH, cuyo modelo por defecto es `deepseek-flash`).
|
|
146
|
+
*/
|
|
147
|
+
const PROVEEDORES_POR_ORDEN = [
|
|
148
|
+
{ variable: 'B_AI_API_KEY', proveedor: 'b-ai', modelo: 'deepseek-v4.1-flash' },
|
|
149
|
+
{ variable: 'OPENROUTER_API_KEY', proveedor: 'openrouter', modelo: 'qwen/qwen3.8-flash' },
|
|
150
|
+
{ variable: 'DEEPSEEK_API_KEY', proveedor: 'deepseek-official', modelo: 'deepseek-flash' },
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
// ── 0 · la puerta: Node 24 ──────────────────────────────────────────────────
|
|
154
|
+
/**
|
|
155
|
+
* Para ANTES de tocar nada si Node es más viejo que {@link NODE_MINIMO}. El
|
|
156
|
+
* motor y sus módulos nativos (`node-pty`, `koffi`, `dsh-subprocess-local`)
|
|
157
|
+
* están probados y precompilados para Node 24; con menos, el fallo saldría más
|
|
158
|
+
* tarde y peor (una terminal que no arranca, un módulo que no carga).
|
|
159
|
+
*/
|
|
160
|
+
function exigirNode() {
|
|
161
|
+
const version = process.versions.node;
|
|
162
|
+
const mayor = Number.parseInt(String(version).split('.')[0], 10);
|
|
163
|
+
if (Number.isFinite(mayor) && mayor >= NODE_MINIMO) return;
|
|
164
|
+
process.stderr.write([
|
|
165
|
+
'',
|
|
166
|
+
'RATACODE · PARA.',
|
|
167
|
+
'',
|
|
168
|
+
' Hace falta Node ' + NODE_MINIMO + ' o más nuevo, y aquí hay Node ' + version + '.',
|
|
169
|
+
' El motor de RATACODE y sus módulos nativos (node-pty, koffi,',
|
|
170
|
+
' subprocess-local) están compilados y probados para Node ' + NODE_MINIMO + ': con menos,',
|
|
171
|
+
' el panel puede arrancar pero la terminal y los subprocesos fallan.',
|
|
172
|
+
'',
|
|
173
|
+
' Qué hacer:',
|
|
174
|
+
' 1. Instala Node ' + NODE_MINIMO + ': https://nodejs.org/es/download',
|
|
175
|
+
' (con nvm-windows: nvm install ' + NODE_MINIMO + ' && nvm use ' + NODE_MINIMO + ')',
|
|
176
|
+
' 2. Vuelve a poner RATACODE: npm i -g ratacode',
|
|
177
|
+
' 3. Arranca: ratacode',
|
|
178
|
+
'',
|
|
179
|
+
' Comprueba tu versión con: node -v',
|
|
180
|
+
' (este RATACODE se está ejecutando con: ' + process.execPath + ')',
|
|
181
|
+
'',
|
|
182
|
+
].join('\n'));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── la línea de órdenes ─────────────────────────────────────────────────────
|
|
187
|
+
function uso() {
|
|
188
|
+
return [
|
|
189
|
+
'RATACODE · la terminal de trabajo bruto',
|
|
190
|
+
'',
|
|
191
|
+
' ratacode [opciones]',
|
|
192
|
+
' ratacode headless "encargo" [opciones]',
|
|
193
|
+
'',
|
|
194
|
+
' --port <n> puerto del panel (por defecto ' + PUERTO_POR_DEFECTO + '; 0 = el que dé el sistema)',
|
|
195
|
+
' --home <ruta> dónde vive la casa de RATACODE (por defecto %USERPROFILE%\\.ratacode)',
|
|
196
|
+
' --carpeta <ruta> espacio de trabajo (por defecto, la carpeta desde la que lanzas ratacode)',
|
|
197
|
+
' --modelo <x> SÓLO headless: el modelo de ese encargo (p. ej. deepseek-v4.1-flash)',
|
|
198
|
+
' --open abre además la URL en el navegador de siempre',
|
|
199
|
+
' -h, --help esto',
|
|
200
|
+
'',
|
|
201
|
+
' ratacode mcp [opciones]',
|
|
202
|
+
'',
|
|
203
|
+
' El servidor MCP de RATACODE, como subcomando del mismo binario:',
|
|
204
|
+
' habla MCP por stdio (lo que espera cualquier cliente) y, con --http,',
|
|
205
|
+
' también por Streamable HTTP. Todo lo que va detrás de «mcp» es suyo.',
|
|
206
|
+
'',
|
|
207
|
+
' ratacode mcp MCP por stdio',
|
|
208
|
+
' ratacode mcp --http MCP por stdio Y por HTTP (puerto 3778; ver mcp/README.md)',
|
|
209
|
+
' ratacode mcp --status enseña el estado de la casa y sale',
|
|
210
|
+
' ratacode mcp --help la ayuda del MCP',
|
|
211
|
+
'',
|
|
212
|
+
' headless no abre panel ni puerto: hace el encargo, imprime la respuesta y termina.',
|
|
213
|
+
'',
|
|
214
|
+
].join('\n');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Las órdenes de RATACODE. `mcp` es un subcomando PASO A PASO: todo lo que va
|
|
219
|
+
* detrás es del servidor MCP (incluido su `--help` y su `--home`), porque por
|
|
220
|
+
* stdio stdout es del protocolo y no se puede tocar.
|
|
221
|
+
*/
|
|
222
|
+
function leerOrdenes(argv) {
|
|
223
|
+
if (argv[0] === 'mcp') {
|
|
224
|
+
const resto = argv.slice(1);
|
|
225
|
+
let casa = null;
|
|
226
|
+
for (let i = 0; i < resto.length; i += 1) {
|
|
227
|
+
const a = resto[i];
|
|
228
|
+
if (a === '--home') casa = resolve(resto[i + 1] ?? '');
|
|
229
|
+
else if (a.startsWith('--home=')) casa = resolve(a.slice(a.indexOf('=') + 1));
|
|
230
|
+
}
|
|
231
|
+
return { modo: 'mcp', mcpArgv: resto, casa, ayuda: false, puerto: PUERTO_POR_DEFECTO, abrir: false, carpeta: null, encargo: null, modelo: null };
|
|
232
|
+
}
|
|
233
|
+
const ordenes = {
|
|
234
|
+
puerto: PUERTO_POR_DEFECTO, casa: null, abrir: false, ayuda: false,
|
|
235
|
+
carpeta: null, modo: 'web', encargo: null, modelo: null, mcpArgv: null,
|
|
236
|
+
};
|
|
237
|
+
const sueltos = [];
|
|
238
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
239
|
+
const a = argv[i];
|
|
240
|
+
if (a === '-h' || a === '--help') ordenes.ayuda = true;
|
|
241
|
+
else if (a === '--open') ordenes.abrir = true;
|
|
242
|
+
else if (a === 'headless') ordenes.modo = 'headless';
|
|
243
|
+
else if (a === '--port' || a.startsWith('--port=')) {
|
|
244
|
+
const valor = a.includes('=') ? a.slice(a.indexOf('=') + 1) : argv[++i];
|
|
245
|
+
if (!/^\d+$/.test(String(valor))) throw new Error('--port tiene que ser un número, y me has dado ' + JSON.stringify(valor));
|
|
246
|
+
ordenes.puerto = Number(valor);
|
|
247
|
+
} else if (a === '--home' || a.startsWith('--home=')) {
|
|
248
|
+
const valor = a.includes('=') ? a.slice(a.indexOf('=') + 1) : argv[++i];
|
|
249
|
+
if (!valor) throw new Error('--home necesita una ruta');
|
|
250
|
+
ordenes.casa = resolve(valor);
|
|
251
|
+
} else if (a === '--carpeta' || a.startsWith('--carpeta=')) {
|
|
252
|
+
const valor = a.includes('=') ? a.slice(a.indexOf('=') + 1) : argv[++i];
|
|
253
|
+
if (!valor) throw new Error('--carpeta necesita una ruta');
|
|
254
|
+
ordenes.carpeta = resolve(valor);
|
|
255
|
+
} else if (a === '--modelo' || a.startsWith('--modelo=')) {
|
|
256
|
+
const valor = a.includes('=') ? a.slice(a.indexOf('=') + 1) : argv[++i];
|
|
257
|
+
if (!valor) throw new Error('--modelo necesita un nombre de modelo');
|
|
258
|
+
ordenes.modelo = valor;
|
|
259
|
+
} else if (a.startsWith('-')) throw new Error('no entiendo «' + a + '» (mira: ratacode --help)');
|
|
260
|
+
else sueltos.push(a);
|
|
261
|
+
}
|
|
262
|
+
if (ordenes.modo === 'headless') {
|
|
263
|
+
if (sueltos.length === 0) {
|
|
264
|
+
throw new Error('headless necesita el encargo entre comillas, por ejemplo: ratacode headless "lista los ficheros"');
|
|
265
|
+
}
|
|
266
|
+
ordenes.encargo = sueltos.join(' ');
|
|
267
|
+
if (ordenes.abrir) throw new Error('--open es del panel; headless no abre nada');
|
|
268
|
+
} else {
|
|
269
|
+
if (sueltos.length > 0) throw new Error('no entiendo «' + sueltos[0] + '» (mira: ratacode --help)');
|
|
270
|
+
if (ordenes.modelo !== null) throw new Error('--modelo es de headless: ratacode headless "encargo" --modelo ' + ordenes.modelo);
|
|
271
|
+
}
|
|
272
|
+
return ordenes;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── 1 · la casa y los perfiles ──────────────────────────────────────────────
|
|
276
|
+
function escribirSiFalta(ruta, texto) {
|
|
277
|
+
if (existsSync(ruta)) return false;
|
|
278
|
+
mkdirSync(dirname(ruta), { recursive: true });
|
|
279
|
+
writeFileSync(ruta, texto);
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Una lista YAML vacía en formato FLUJO, sola en su línea: `[]` (o `[ ]`). */
|
|
284
|
+
const LINEA_VACIA_FLUJO = /^[ \t]*\[[ \t]*\][ \t]*$/;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Las filas de un `cordis.patch.yml`, o `null` si ese texto NO es una lista
|
|
288
|
+
* YAML válida (que es el caso roto de la 0.2.0). Un fichero vacío es la lista
|
|
289
|
+
* vacía; un documento que no sea una lista tampoco vale.
|
|
290
|
+
* @param texto - el contenido del fichero.
|
|
291
|
+
* @returns las filas, o null.
|
|
292
|
+
*/
|
|
293
|
+
function filasDelParche(texto) {
|
|
294
|
+
let cargado;
|
|
295
|
+
try {
|
|
296
|
+
cargado = yaml.load(texto);
|
|
297
|
+
} catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
if (cargado === null || cargado === undefined) return [];
|
|
301
|
+
return Array.isArray(cargado) ? cargado : null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** ¿Estas filas ya traen la capa de presets? */
|
|
305
|
+
function tieneLaCapa(filas) {
|
|
306
|
+
return filas.some((fila) => fila !== null && typeof fila === 'object' && !Array.isArray(fila) && fila.id === 'agent-presets');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Deja la capa de presets en el `cordis.patch.yml` de un perfil, SIEMPRE como
|
|
311
|
+
* lista YAML válida. La plantilla sólo se escribe cuando el fichero falta
|
|
312
|
+
* (`escribirSiFalta`), así que sin esto una casa estrenada con una versión
|
|
313
|
+
* anterior seguiría enseñando los modos de serie del motor.
|
|
314
|
+
*
|
|
315
|
+
* Tres casos, los tres medidos:
|
|
316
|
+
* · el fichero trae `[]` o va vacío → se escribe la capa sola;
|
|
317
|
+
* · el fichero trae filas del usuario → se respetan y la capa se añade al
|
|
318
|
+
* final (que es donde manda);
|
|
319
|
+
* · el fichero se quedó ROTO por la 0.2.0 —un `[]` pegado delante de las
|
|
320
|
+
* filas, que no es YAML— → se quita esa línea suelta y se reescribe válido,
|
|
321
|
+
* sin perder ni una fila del usuario. Pasaba al actualizar una casa de la
|
|
322
|
+
* 0.1 y el motor moría: «YAMLException: end of the stream or a document
|
|
323
|
+
* separator is expected».
|
|
324
|
+
* @param perfil - la carpeta del perfil (`<casa>/profiles/web`).
|
|
325
|
+
* @returns `{puesto, reparado, motivo}`.
|
|
326
|
+
*/
|
|
327
|
+
function asegurarCapaPresets(perfil) {
|
|
328
|
+
const ruta = join(perfil, 'cordis.patch.yml');
|
|
329
|
+
let texto = '';
|
|
330
|
+
try {
|
|
331
|
+
texto = readFileSync(ruta, 'utf8');
|
|
332
|
+
} catch {
|
|
333
|
+
return { puesto: false, reparado: false, motivo: 'sin cordis.patch.yml' };
|
|
334
|
+
}
|
|
335
|
+
let filas = filasDelParche(texto);
|
|
336
|
+
let reparado = false;
|
|
337
|
+
if (filas === null) {
|
|
338
|
+
const limpio = texto.split('\n').filter((linea) => !LINEA_VACIA_FLUJO.test(linea)).join('\n');
|
|
339
|
+
filas = filasDelParche(limpio);
|
|
340
|
+
if (filas === null) return { puesto: false, reparado: false, motivo: 'el cordis.patch.yml del perfil no se puede leer como YAML: no lo toco' };
|
|
341
|
+
reparado = true;
|
|
342
|
+
texto = limpio;
|
|
343
|
+
}
|
|
344
|
+
if (tieneLaCapa(filas)) {
|
|
345
|
+
if (reparado) escribir(ruta, texto);
|
|
346
|
+
return { puesto: false, reparado, motivo: reparado ? 'ya tenía la capa' : 'ya estaba' };
|
|
347
|
+
}
|
|
348
|
+
if (filas.length === 0) {
|
|
349
|
+
escribir(ruta, PLANTILLA_PARCHE);
|
|
350
|
+
return { puesto: true, reparado, motivo: reparado ? 'estaba vacío' : 'estaba vacío' };
|
|
351
|
+
}
|
|
352
|
+
escribir(ruta, (texto.endsWith('\n') ? texto + '\n' : texto + '\n\n') + CAPA_PRESETS);
|
|
353
|
+
return { puesto: true, reparado, motivo: reparado ? 'capa añadida al final' : 'añadida al final' };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** El perfil: sus bundles, en orden. Si ya existe, NO se pisa: se completa. */
|
|
357
|
+
function prepararPerfil(perfil, nombre, bundles, patchReload) {
|
|
358
|
+
const manifest = join(perfil, 'package.json');
|
|
359
|
+
let pkg;
|
|
360
|
+
if (existsSync(manifest)) {
|
|
361
|
+
pkg = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
362
|
+
} else {
|
|
363
|
+
pkg = { name: nombre, private: true, dependencies: {} };
|
|
364
|
+
}
|
|
365
|
+
pkg.dsh = pkg.dsh ?? {};
|
|
366
|
+
pkg.dsh.profile = pkg.dsh.profile ?? {};
|
|
367
|
+
const lista = (pkg.dsh.profile.bundles = pkg.dsh.profile.bundles ?? []);
|
|
368
|
+
for (const b of bundles) {
|
|
369
|
+
if (!lista.includes(b)) lista.push(b);
|
|
370
|
+
}
|
|
371
|
+
pkg.dsh.profile.patchReload = pkg.dsh.profile.patchReload ?? patchReload;
|
|
372
|
+
mkdirSync(perfil, { recursive: true });
|
|
373
|
+
writeFileSync(manifest, JSON.stringify(pkg, null, 2) + '\n');
|
|
374
|
+
escribirSiFalta(join(perfil, 'cordis.patch.yml'), PLANTILLA_PARCHE);
|
|
375
|
+
escribirSiFalta(join(perfil, 'pnpm-workspace.yaml'), PLANTILLA_PNPM);
|
|
376
|
+
const capa = asegurarCapaPresets(perfil);
|
|
377
|
+
return capa;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Copia un árbol de ficheros escribiendo SÓLO lo que cambia: lo que ya está
|
|
382
|
+
* igual no se toca (y se cuenta aparte). Se usa para la piel dentro del perfil
|
|
383
|
+
* y para los modos dentro de la casa.
|
|
384
|
+
*/
|
|
385
|
+
function copiarArbol(origen, destino, cuenta) {
|
|
386
|
+
mkdirSync(destino, { recursive: true });
|
|
387
|
+
for (const entrada of readdirSync(origen, { withFileTypes: true })) {
|
|
388
|
+
if (entrada.name === 'node_modules' || entrada.name === '.git') continue;
|
|
389
|
+
const o = join(origen, entrada.name);
|
|
390
|
+
const d = join(destino, entrada.name);
|
|
391
|
+
if (entrada.isDirectory()) {
|
|
392
|
+
copiarArbol(o, d, cuenta);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const contenido = readFileSync(o);
|
|
396
|
+
if (existsSync(d) && statSync(d).isFile() && Buffer.compare(readFileSync(d), contenido) === 0) {
|
|
397
|
+
cuenta.iguales += 1;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
writeFileSync(d, contenido);
|
|
401
|
+
cuenta.copiados += 1;
|
|
402
|
+
}
|
|
403
|
+
return cuenta;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Escribe un fichero de texto entero, creando la carpeta que haga falta. */
|
|
407
|
+
function escribir(ruta, texto) {
|
|
408
|
+
mkdirSync(dirname(ruta), { recursive: true });
|
|
409
|
+
writeFileSync(ruta, texto, { mode: 0o600 });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ── 2 · la casa: estreno, aviso, modos y modelo por defecto ─────────────────
|
|
413
|
+
/** 2a · El aviso «Internal Testing», dado por aceptado (sin pisar lo que ya haya). */
|
|
414
|
+
function aceptarAviso(casa) {
|
|
415
|
+
const ruta = join(casa, 'settings.yaml');
|
|
416
|
+
let actual = '';
|
|
417
|
+
try { actual = readFileSync(ruta, 'utf8'); } catch { actual = ''; }
|
|
418
|
+
if (/welcomeNoticeVersion\s*:/.test(actual)) return 'ya estaba';
|
|
419
|
+
const seccion = (actual.trim() === '' ? '' : actual.replace(/\s*$/, '\n'))
|
|
420
|
+
+ 'ui-onboarding:\n welcomeNoticeVersion: ' + AVISO_ACEPTADO + '\n';
|
|
421
|
+
mkdirSync(casa, { recursive: true });
|
|
422
|
+
writeFileSync(ruta, seccion, { mode: 0o600 });
|
|
423
|
+
return 'puesto';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** El primer proveedor con clave en el entorno. SÓLO se mira si existe. */
|
|
427
|
+
function primerProveedorConClave(entorno) {
|
|
428
|
+
for (const candidato of PROVEEDORES_POR_ORDEN) {
|
|
429
|
+
const valor = entorno[candidato.variable];
|
|
430
|
+
// Un export vacío es «no puesta» por convención del shell. Nunca se mira
|
|
431
|
+
// el contenido: sólo si hay algo.
|
|
432
|
+
if (typeof valor === 'string' && valor.trim() !== '') return candidato;
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** El `settings.yaml` de una casa, como objeto; null si no se puede leer. */
|
|
438
|
+
function leerAjustes(casa) {
|
|
439
|
+
try {
|
|
440
|
+
return yaml.load(readFileSync(join(casa, 'settings.yaml'), 'utf8')) ?? {};
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** El primer modelo que la casa declara para un proveedor, si declara alguno. */
|
|
447
|
+
function primerModeloDeclarado(ajustes, proveedor) {
|
|
448
|
+
const declarado = ajustes?.['llm-pi-ai']?.providers?.[proveedor]?.models;
|
|
449
|
+
if (!Array.isArray(declarado) || declarado.length === 0) return null;
|
|
450
|
+
const primero = declarado[0];
|
|
451
|
+
return typeof primero === 'string' ? primero : (primero?.id ?? null);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Reescribe el bloque `agent-default-model:` de un `settings.yaml` sin tocar
|
|
456
|
+
* nada más: ni los comentarios, ni los proveedores, ni el orden del documento.
|
|
457
|
+
* Si el bloque no está, se añade al final.
|
|
458
|
+
*/
|
|
459
|
+
function ponerModeloEnTexto(texto, proveedor, modelo) {
|
|
460
|
+
const bloque = 'agent-default-model:\n provider: ' + proveedor + '\n model: ' + JSON.stringify(modelo) + '\n';
|
|
461
|
+
const bloqueActual = /^agent-default-model:[ \t]*\n(?:[ \t]+[^\n]*\n)*/m;
|
|
462
|
+
if (bloqueActual.test(texto)) return texto.replace(bloqueActual, bloque);
|
|
463
|
+
return (texto.trim() === '' ? '' : texto.replace(/\s*$/, '\n')) + '\n' + bloque;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* La casa: la PRIMERA vez se estrena con los ajustes de fábrica (proveedores,
|
|
468
|
+
* aviso aceptado y permiso sin preguntar) y con el modelo del primer proveedor
|
|
469
|
+
* con clave. Si ya existe, NO se pisa nada del usuario.
|
|
470
|
+
*/
|
|
471
|
+
function estrenarCasa(casa, entorno) {
|
|
472
|
+
const ruta = join(casa, 'settings.yaml');
|
|
473
|
+
if (existsSync(ruta)) return { nueva: false, aviso: aceptarAviso(casa), modelo: null };
|
|
474
|
+
mkdirSync(casa, { recursive: true });
|
|
475
|
+
let texto = readFileSync(FABRICA_ORIGEN, 'utf8');
|
|
476
|
+
const elegido = primerProveedorConClave(entorno);
|
|
477
|
+
if (elegido !== null) {
|
|
478
|
+
const ajustes = yaml.load(texto) ?? {};
|
|
479
|
+
const modelo = primerModeloDeclarado(ajustes, elegido.proveedor) ?? elegido.modelo;
|
|
480
|
+
texto = ponerModeloEnTexto(texto, elegido.proveedor, modelo);
|
|
481
|
+
writeFileSync(ruta, texto, { mode: 0o600 });
|
|
482
|
+
return {
|
|
483
|
+
nueva: true,
|
|
484
|
+
aviso: 'puesto (fábrica)',
|
|
485
|
+
modelo: { proveedor: elegido.proveedor, modelo, variable: elegido.variable },
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
writeFileSync(ruta, texto, { mode: 0o600 });
|
|
489
|
+
return { nueva: true, aviso: 'puesto (fábrica)', modelo: null };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Deja el `settings.yaml` de una casa con `agent-presets.default: modo-rata`
|
|
494
|
+
* cuando el valor que hay es uno de los que HAY QUE migrar
|
|
495
|
+
* ({@link PRESETS_A_MIGRAR}: el `standard` de fábrica, los tres modos que ya no
|
|
496
|
+
* existen y los de serie del motor, que no resuelven con la raíz de serie
|
|
497
|
+
* apagada). Un valor elegido por el usuario —uno de los 9 modos de la casa, o su
|
|
498
|
+
* propio preset— se respeta: no se pelea con quien manda en su casa.
|
|
499
|
+
* Se hace sobre el TEXTO (como `ponerModeloEnTexto`) para no llevarse por
|
|
500
|
+
* delante comentarios ni el orden del documento.
|
|
501
|
+
*/
|
|
502
|
+
function ponerPresetPorDefecto(casa, preset = PRESET_POR_DEFECTO) {
|
|
503
|
+
const ruta = join(casa, 'settings.yaml');
|
|
504
|
+
if (!existsSync(ruta)) return { cambiado: false, motivo: 'la casa no tiene settings.yaml' };
|
|
505
|
+
const texto = readFileSync(ruta, 'utf8');
|
|
506
|
+
let actual;
|
|
507
|
+
try {
|
|
508
|
+
actual = (yaml.load(texto) ?? {})?.['agent-presets']?.default;
|
|
509
|
+
} catch {
|
|
510
|
+
return { cambiado: false, motivo: 'el settings.yaml de la casa no se puede leer como YAML: no lo toco' };
|
|
511
|
+
}
|
|
512
|
+
if (actual === preset) return { cambiado: false, motivo: 'ya estaba en ' + preset };
|
|
513
|
+
if (typeof actual === 'string' && actual.trim() !== '' && !PRESETS_A_MIGRAR.includes(actual)) {
|
|
514
|
+
return { cambiado: false, motivo: 'la casa tiene «' + actual + '» puesto a mano: se respeta' };
|
|
515
|
+
}
|
|
516
|
+
const bloque = 'agent-presets:\n default: ' + preset + '\n';
|
|
517
|
+
const bloqueActual = /^agent-presets:[ \t]*\n(?:[ \t]+[^\n]*\n)*/m;
|
|
518
|
+
let nuevo;
|
|
519
|
+
if (bloqueActual.test(texto)) nuevo = texto.replace(bloqueActual, bloque);
|
|
520
|
+
else if (/^agent-presets:[ \t]*\S.*$/m.test(texto)) nuevo = texto.replace(/^agent-presets:[ \t]*\S.*$/m, bloque.trimEnd());
|
|
521
|
+
else nuevo = (texto.trim() === '' ? '' : texto.replace(/\s*$/, '\n')) + '\n' + bloque;
|
|
522
|
+
writeFileSync(ruta, nuevo, { mode: 0o600 });
|
|
523
|
+
return { cambiado: true, antes: actual ?? '(no estaba)', motivo: (actual ?? '(no estaba)') + ' → ' + preset };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* El IDIOMA de la casa: español (R21). DSH guarda el idioma elegido en
|
|
528
|
+
* `settings.yaml` → `locale.preference` (esquema del paquete
|
|
529
|
+
* `@deepseek-ai/dsh-client-locale`: `{preference: <etiqueta BCP 47>}`) y, si no
|
|
530
|
+
* hay nada apuntado, se lo pregunta al navegador. RATACODE es una casa en
|
|
531
|
+
* español, así que se apunta «es» —pero SÓLO si la casa no ha elegido ya otro
|
|
532
|
+
* idioma: la elección del usuario no se toca nunca—. Se escribe sobre el TEXTO,
|
|
533
|
+
* como `ponerPresetPorDefecto`, para no llevarse por delante comentarios ni el
|
|
534
|
+
* orden del documento.
|
|
535
|
+
* @param casa - la casa de RATACODE.
|
|
536
|
+
* @returns `{cambiado, motivo}`.
|
|
537
|
+
*/
|
|
538
|
+
function ponerIdiomaPorDefecto(casa, idioma = IDIOMA_POR_DEFECTO) {
|
|
539
|
+
const ruta = join(casa, 'settings.yaml');
|
|
540
|
+
if (!existsSync(ruta)) return { cambiado: false, motivo: 'la casa no tiene settings.yaml' };
|
|
541
|
+
const texto = readFileSync(ruta, 'utf8');
|
|
542
|
+
let actual;
|
|
543
|
+
try {
|
|
544
|
+
actual = (yaml.load(texto) ?? {})?.locale?.preference;
|
|
545
|
+
} catch {
|
|
546
|
+
return { cambiado: false, motivo: 'el settings.yaml de la casa no se puede leer como YAML: no lo toco' };
|
|
547
|
+
}
|
|
548
|
+
if (actual === idioma) return { cambiado: false, motivo: 'ya estaba en ' + idioma };
|
|
549
|
+
if (typeof actual === 'string' && actual.trim() !== '') {
|
|
550
|
+
return { cambiado: false, motivo: 'la casa tiene «' + actual + '» puesto a mano: se respeta' };
|
|
551
|
+
}
|
|
552
|
+
const bloque = 'locale:\n preference: ' + idioma + '\n';
|
|
553
|
+
const bloqueActual = /^locale:[ \t]*\n(?:[ \t]+[^\n]*\n)*/m;
|
|
554
|
+
let nuevo;
|
|
555
|
+
if (bloqueActual.test(texto)) nuevo = texto.replace(bloqueActual, bloque);
|
|
556
|
+
else nuevo = (texto.trim() === '' ? '' : texto.replace(/\s*$/, '\n')) + '\n' + bloque;
|
|
557
|
+
writeFileSync(ruta, nuevo, { mode: 0o600 });
|
|
558
|
+
return { cambiado: true, antes: actual ?? '(no estaba)', motivo: (actual ?? '(no estaba)') + ' → ' + idioma };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* El ASPECTO de la casa (R21): los tres temas (RATACODE PINK, RATACODE YELLOW
|
|
563
|
+
* y MINIMAL) los aplica la vía OFICIAL de temas de DSH, pero el motor no admite
|
|
564
|
+
* un id de tema de fuera en su esquema de ajustes (`light`/`dark`/`system`,
|
|
565
|
+
* `dsh-client-ui-theme`: `THEME_PREFERENCES`), así que CUÁL está puesto se
|
|
566
|
+
* apunta en `<casa>\tema.txt` (una palabra) y la piel lo vuelve a poner al
|
|
567
|
+
* abrir. Aquí sólo se deja apuntado el de fábrica: no se pisa una elección.
|
|
568
|
+
*/
|
|
569
|
+
function ponerTemaPorDefecto(casa, tema = TEMA_POR_DEFECTO) {
|
|
570
|
+
const ruta = join(casa, 'tema.txt');
|
|
571
|
+
if (existsSync(ruta)) {
|
|
572
|
+
const leido = readFileSync(ruta, 'utf8').trim();
|
|
573
|
+
if (TEMAS_DE_LA_CASA.includes(leido)) return { cambiado: false, motivo: 'ya estaba en ' + leido };
|
|
574
|
+
}
|
|
575
|
+
mkdirSync(casa, { recursive: true });
|
|
576
|
+
writeFileSync(ruta, tema + '\n', { mode: 0o600 });
|
|
577
|
+
return { cambiado: true, motivo: tema };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Los modos, EN TODA CASA (no sólo al estrenar): copia los 9 de RATACODE,
|
|
582
|
+
* borra los presets que salieron en versiones anteriores (el enlazador, el
|
|
583
|
+
* promptista y el escritor: `copiarArbol` sólo escribe, nunca borra) y pasa
|
|
584
|
+
* `agent-presets.default` a `modo-rata` si apunta a algo que ya no resuelve
|
|
585
|
+
* (ver {@link ponerPresetPorDefecto}). La elección del usuario se respeta.
|
|
586
|
+
*/
|
|
587
|
+
function asegurarModos(casa) {
|
|
588
|
+
const presets = join(casa, '.agent-presets');
|
|
589
|
+
const cuenta = copiarArbol(MODOS_ORIGEN, presets, { copiados: 0, iguales: 0 });
|
|
590
|
+
const borrados = [];
|
|
591
|
+
for (const viejo of PRESETS_VIEJOS) {
|
|
592
|
+
const ruta = join(presets, viejo);
|
|
593
|
+
if (!existsSync(ruta)) continue;
|
|
594
|
+
try {
|
|
595
|
+
rmSync(ruta, { recursive: true, force: true });
|
|
596
|
+
borrados.push(viejo);
|
|
597
|
+
} catch { /* si no se deja borrar, el aviso de abajo lo dice */ }
|
|
598
|
+
}
|
|
599
|
+
return { cuenta, borrados, preset: ponerPresetPorDefecto(casa), presets };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ── 3 · el espacio de trabajo ───────────────────────────────────────────────
|
|
603
|
+
/**
|
|
604
|
+
* Registra una carpeta en el registro de espacios de DSH
|
|
605
|
+
* (`<casa>\storages\workspace.json`), con el formato medido de
|
|
606
|
+
* `@deepseek-ai/dsh-workspace` (`lib/types/spec.js`): dominio `workspace`
|
|
607
|
+
* versión 2, tabla `workspaces` con `{path,title,sessionIds,createdAt,updatedAt}`
|
|
608
|
+
* y el global `{initialized, workspaceIds, archivedSessionIds}` donde
|
|
609
|
+
* `workspaceIds` es el orden de la barra lateral (el nuevo va el primero).
|
|
610
|
+
*
|
|
611
|
+
* La ruta se guarda CANÓNICA (`fs.realpath`), que es la única canon de
|
|
612
|
+
* identidad del registro: si no, DSH crearía un segundo espacio para el mismo
|
|
613
|
+
* sitio. Si el espacio ya está, no se toca (ni el orden ni el título).
|
|
614
|
+
*/
|
|
615
|
+
function registrarEspacio(casa, carpeta) {
|
|
616
|
+
const canonica = realpathSync(carpeta);
|
|
617
|
+
const ruta = join(casa, 'storages', 'workspace.json');
|
|
618
|
+
let doc = null;
|
|
619
|
+
if (existsSync(ruta)) {
|
|
620
|
+
try { doc = JSON.parse(readFileSync(ruta, 'utf8')); } catch { doc = null; }
|
|
621
|
+
}
|
|
622
|
+
if (doc !== null && doc?.unit?.name !== 'workspace') doc = null;
|
|
623
|
+
if (doc !== null && doc.unit.version !== 2) {
|
|
624
|
+
return { registrado: false, motivo: 'el registro de espacios de esta casa es versión ' + doc.unit.version
|
|
625
|
+
+ ', y yo sé escribir la 2: no lo toco' };
|
|
626
|
+
}
|
|
627
|
+
if (doc === null) {
|
|
628
|
+
doc = {
|
|
629
|
+
unit: { name: 'workspace', version: 2 },
|
|
630
|
+
global: { initialized: true, workspaceIds: [], archivedSessionIds: [] },
|
|
631
|
+
tables: { workspaces: {} },
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
doc.global = doc.global ?? {};
|
|
635
|
+
if (!Array.isArray(doc.global.workspaceIds)) doc.global.workspaceIds = [];
|
|
636
|
+
if (!Array.isArray(doc.global.archivedSessionIds)) doc.global.archivedSessionIds = [];
|
|
637
|
+
doc.tables = doc.tables ?? {};
|
|
638
|
+
doc.tables.workspaces = doc.tables.workspaces ?? {};
|
|
639
|
+
for (const [id, registro] of Object.entries(doc.tables.workspaces)) {
|
|
640
|
+
if (registro?.path === canonica) return { registrado: true, nuevo: false, id, canonica };
|
|
641
|
+
}
|
|
642
|
+
const id = randomUUID();
|
|
643
|
+
const ahora = new Date().toISOString();
|
|
644
|
+
doc.tables.workspaces[id] = {
|
|
645
|
+
path: canonica,
|
|
646
|
+
title: basename(canonica) || canonica,
|
|
647
|
+
sessionIds: [],
|
|
648
|
+
createdAt: ahora,
|
|
649
|
+
updatedAt: ahora,
|
|
650
|
+
};
|
|
651
|
+
doc.global.workspaceIds = [id, ...doc.global.workspaceIds];
|
|
652
|
+
doc.global.initialized = true;
|
|
653
|
+
mkdirSync(dirname(ruta), { recursive: true });
|
|
654
|
+
const temporal = ruta + '.' + process.pid + '.tmp';
|
|
655
|
+
writeFileSync(temporal, JSON.stringify(doc, null, 2) + '\n', { mode: 0o600 });
|
|
656
|
+
renameSync(temporal, ruta);
|
|
657
|
+
return { registrado: true, nuevo: true, id, canonica };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** Comprueba que una carpeta existe y es una carpeta, o para con un mensaje claro. */
|
|
661
|
+
function exigirCarpeta(carpeta) {
|
|
662
|
+
if (!existsSync(carpeta)) throw new Error('la carpeta de trabajo no existe: ' + carpeta);
|
|
663
|
+
if (!statSync(carpeta).isDirectory()) throw new Error('la carpeta de trabajo no es una carpeta: ' + carpeta);
|
|
664
|
+
return carpeta;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ── 4 · el motor ────────────────────────────────────────────────────────────
|
|
668
|
+
function binDelMotor() {
|
|
669
|
+
const require = createRequire(import.meta.url);
|
|
670
|
+
const manifiesto = require.resolve('@deepseek-ai/dsh/package.json');
|
|
671
|
+
const pkg = JSON.parse(readFileSync(manifiesto, 'utf8'));
|
|
672
|
+
const rel = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.dsh;
|
|
673
|
+
if (!rel) throw new Error('el motor no declara su binario (¿instalación a medias?)');
|
|
674
|
+
const bin = join(dirname(manifiesto), rel);
|
|
675
|
+
if (!existsSync(bin)) throw new Error('no encuentro el motor en ' + bin);
|
|
676
|
+
return { bin, version: pkg.version };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* El parche que fija el selector de carpeta EN LA WEB. `dsh-web-app` trae
|
|
681
|
+
* `dsh-host-directory-picker-auto`, que resuelve «native» en Windows y abre el
|
|
682
|
+
* diálogo del sistema: una ventana que quien maneja RATACODE desde fuera (un
|
|
683
|
+
* agente, un chat) no puede ver ni contestar. DSH documenta el punto de
|
|
684
|
+
* intercambio: componer el backend «browse» directamente. Hay que montar las
|
|
685
|
+
* DOS caras (backend de host + superficie de cliente); con una sola, la
|
|
686
|
+
* ventana no sale o la fila «Añadir espacio…» no aparece.
|
|
687
|
+
*/
|
|
688
|
+
function parcheSelectorCarpeta(casa) {
|
|
689
|
+
const ruta = join(casa, 'perfiles-parche', 'web-selector-carpeta.yml');
|
|
690
|
+
escribir(ruta, [
|
|
691
|
+
'# RATACODE · el selector de carpeta, EN LA WEB (nunca una ventana de Windows).',
|
|
692
|
+
'# Se apaga la fila adaptativa de dsh-web-app y se compone el par «browse».',
|
|
693
|
+
'- id: directory-picker',
|
|
694
|
+
' disabled: true',
|
|
695
|
+
'',
|
|
696
|
+
'- insert:',
|
|
697
|
+
" - id: directory-picker-browse",
|
|
698
|
+
" name: '@deepseek-ai/dsh-host-directory-picker-browse'",
|
|
699
|
+
'',
|
|
700
|
+
" - id: ui-directory-picker-browse",
|
|
701
|
+
" name: '@deepseek-ai/dsh-client-ui-directory-picker-browse'",
|
|
702
|
+
'',
|
|
703
|
+
].join('\n'));
|
|
704
|
+
return ruta;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Resuelve `--modelo X` contra los proveedores que la casa declara.
|
|
709
|
+
* Acepta `proveedor:modelo` y, si no, busca qué proveedor declara ese modelo.
|
|
710
|
+
* Si no lo declara ninguno, usa el proveedor por defecto con ese modelo.
|
|
711
|
+
*/
|
|
712
|
+
function resolverModelo(casa, pedido, porDefecto) {
|
|
713
|
+
if (pedido.includes(':')) {
|
|
714
|
+
const [proveedor, ...resto] = pedido.split(':');
|
|
715
|
+
return { proveedor, modelo: resto.join(':') };
|
|
716
|
+
}
|
|
717
|
+
const ajustes = leerAjustes(casa) ?? {};
|
|
718
|
+
const proveedores = ajustes?.['llm-pi-ai']?.providers ?? {};
|
|
719
|
+
for (const [proveedor, perfil] of Object.entries(proveedores)) {
|
|
720
|
+
const modelos = Array.isArray(perfil?.models) ? perfil.models : [];
|
|
721
|
+
for (const m of modelos) {
|
|
722
|
+
if ((typeof m === 'string' ? m : m?.id) === pedido) return { proveedor, modelo: pedido };
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const actual = ajustes?.['agent-default-model'] ?? porDefecto ?? PROVEEDORES_POR_ORDEN[0];
|
|
726
|
+
return { proveedor: actual.provider ?? actual.proveedor, modelo: pedido };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* El parche de `--modelo`: el documento de ajustes del motor pasa a ser una
|
|
731
|
+
* COPIA con el modelo cambiado. Así el encargo usa el modelo pedido sin tocar
|
|
732
|
+
* el `settings.yaml` del usuario (la capa de usuario de DSH manda sobre la
|
|
733
|
+
* composición, así que un parche de composición no bastaría).
|
|
734
|
+
*/
|
|
735
|
+
function parcheModelo(casa, eleccion) {
|
|
736
|
+
const original = join(casa, 'settings.yaml');
|
|
737
|
+
const texto = existsSync(original) ? readFileSync(original, 'utf8') : '';
|
|
738
|
+
const copia = join(casa, 'perfiles-parche', 'headless-' + process.pid + '-settings.yaml');
|
|
739
|
+
escribir(copia, ponerModeloEnTexto(texto, eleccion.proveedor, eleccion.modelo));
|
|
740
|
+
const ruta = join(casa, 'perfiles-parche', 'headless-modelo.yml');
|
|
741
|
+
escribir(ruta, [
|
|
742
|
+
'# RATACODE · sólo para ESTE encargo: los ajustes son una copia con el modelo',
|
|
743
|
+
'# cambiado, así no se toca el modelo que el usuario tenga guardado.',
|
|
744
|
+
'- id: settings',
|
|
745
|
+
' config:',
|
|
746
|
+
' path: ' + JSON.stringify(copia),
|
|
747
|
+
'',
|
|
748
|
+
].join('\n'));
|
|
749
|
+
return { parche: ruta, copia };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function matarArbol(hijo) {
|
|
753
|
+
if (!hijo || hijo.killed || hijo.pid === undefined) return;
|
|
754
|
+
try {
|
|
755
|
+
if (process.platform === 'win32') {
|
|
756
|
+
spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', 'taskkill /pid ' + hijo.pid + '/T /F'], { windowsHide: true, stdio: 'ignore' });
|
|
757
|
+
} else hijo.kill('SIGTERM');
|
|
758
|
+
} catch { /* el hijo ya se fue */ }
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* ¿Hay alguien escuchando ya en ese puerto? Se pregunta ANTES de arrancar el
|
|
763
|
+
* motor: si está ocupado, el fallo del motor son 45 líneas de traza en inglés
|
|
764
|
+
* con rutas internas (`EADDRINUSE`), y el usuario nuevo se come todas.
|
|
765
|
+
* @param {number} puerto - puerto a probar.
|
|
766
|
+
* @param {string} host - a quién preguntar (loopback).
|
|
767
|
+
* @returns {Promise<boolean>}
|
|
768
|
+
*/
|
|
769
|
+
function puertoOcupado(puerto, host = '127.0.0.1') {
|
|
770
|
+
return new Promise((listo) => {
|
|
771
|
+
const s = connect({ host, port: puerto });
|
|
772
|
+
s.setTimeout(1000);
|
|
773
|
+
const terminar = (ocupado) => { s.destroy(); listo(ocupado); };
|
|
774
|
+
s.once('connect', () => terminar(true));
|
|
775
|
+
s.once('error', () => terminar(false));
|
|
776
|
+
s.once('timeout', () => terminar(false));
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* ¿Hay credencial para este proveedor? Se mira el ENTORNO y, si el fichero de
|
|
782
|
+
* claves de la casa existe, se da por buena (la resuelve el motor). Nunca se lee
|
|
783
|
+
* el valor de nada.
|
|
784
|
+
* @param {string} casa - la casa de RATACODE.
|
|
785
|
+
* @param {object} ajustes - el settings.yaml ya leído.
|
|
786
|
+
* @param {string} proveedor - la ruta del proveedor.
|
|
787
|
+
* @returns {{nombre: string, esta: boolean, enLaCasa: boolean}|null} null si la ruta no declara credencial.
|
|
788
|
+
*/
|
|
789
|
+
function credencialDe(casa, ajustes, proveedor) {
|
|
790
|
+
const enLaCasa = existsSync(join(casa, '.credentials.yaml'));
|
|
791
|
+
if (proveedor === 'deepseek-official') {
|
|
792
|
+
const nombre = ajustes?.['llm-deepseek']?.apiKeyEnv ?? 'DEEPSEEK_API_KEY';
|
|
793
|
+
return { nombre, esta: hayVariable(nombre), enLaCasa };
|
|
794
|
+
}
|
|
795
|
+
const perfil = ajustes?.['llm-pi-ai']?.providers?.[proveedor];
|
|
796
|
+
const nombre = perfil && typeof perfil === 'object' ? perfil.apiKeyEnv : undefined;
|
|
797
|
+
if (typeof nombre !== 'string' || nombre.trim() === '') return null;
|
|
798
|
+
return { nombre, esta: hayVariable(nombre), enLaCasa };
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** Las claves ya NO vienen del entorno: el motor arranca sin ellas (ver `entornoSinClaves`). */
|
|
802
|
+
function hayVariable() {
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** Dónde el panel deja dicho que el usuario ya vio el aviso de migración. */
|
|
807
|
+
function rutaAvisoVisto(casa) {
|
|
808
|
+
return join(casa, 'aviso-claves-visto');
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** Dónde se dejan los NOMBRES de las variables de claves que hay en Windows. */
|
|
812
|
+
function rutaAvisoClaves(casa) {
|
|
813
|
+
return join(casa, 'aviso-claves.txt');
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* R22 §4 · La migración, dicha UNA vez. La primera vez que una casa arranca con
|
|
818
|
+
* esta versión, si en Windows hay variables de claves de proveedores (de las que
|
|
819
|
+
* RATACODE ya NO hace caso), se dejan sus NOMBRES —nunca sus valores— en
|
|
820
|
+
* `<casa>\aviso-claves.txt`, y el panel lo enseña en una línea. El panel
|
|
821
|
+
* comprueba además que esa clave no esté ya puesta en la casa: si está, no hay
|
|
822
|
+
* nada que avisar. Cuando el usuario lo cierra, queda `<casa>\aviso-claves-visto`
|
|
823
|
+
* y no vuelve.
|
|
824
|
+
* @param {string} casa - la casa de RATACODE.
|
|
825
|
+
* @returns {string[]} los nombres encontrados (para el registro).
|
|
826
|
+
*/
|
|
827
|
+
function anotarVariablesDeClaves(casa) {
|
|
828
|
+
const ruta = rutaAvisoClaves(casa);
|
|
829
|
+
if (existsSync(rutaAvisoVisto(casa))) {
|
|
830
|
+
try { rmSync(ruta, { force: true }); } catch { /* da igual */ }
|
|
831
|
+
return [];
|
|
832
|
+
}
|
|
833
|
+
const nombres = new Set([
|
|
834
|
+
'B_AI_API_KEY', 'BAI_API_KEY', 'OPENROUTER_API_KEY', 'DEEPSEEK_API_KEY', 'GROQ_API_KEY',
|
|
835
|
+
'GEMINI_API_KEY', 'NVIDIA_API_KEY', 'SAMBANOVA_API_KEY', 'CLOUDFLARE_API_KEY', 'CLOUDFLARE_API_TOKEN',
|
|
836
|
+
]);
|
|
837
|
+
const ajustes = leerAjustes(casa) ?? {};
|
|
838
|
+
const nativa = ajustes?.['llm-deepseek']?.apiKeyEnv;
|
|
839
|
+
if (typeof nativa === 'string' && nativa.trim() !== '') nombres.add(nativa.trim());
|
|
840
|
+
for (const perfil of Object.values(ajustes?.['llm-pi-ai']?.providers ?? {})) {
|
|
841
|
+
if (perfil && typeof perfil.apiKeyEnv === 'string' && perfil.apiKeyEnv.trim() !== '') nombres.add(perfil.apiKeyEnv.trim());
|
|
842
|
+
}
|
|
843
|
+
// SÓLO se mira si la variable existe: su valor no se lee ni se copia a ningún sitio.
|
|
844
|
+
const puestas = [...nombres].filter((nombre) => {
|
|
845
|
+
const valor = process.env[nombre];
|
|
846
|
+
return typeof valor === 'string' && valor.trim() !== '';
|
|
847
|
+
});
|
|
848
|
+
try {
|
|
849
|
+
if (puestas.length === 0) rmSync(ruta, { force: true });
|
|
850
|
+
else writeFileSync(ruta, puestas.join('\n') + '\n', { mode: 0o600 });
|
|
851
|
+
} catch { /* el aviso es una ayuda, no un requisito para arrancar */ }
|
|
852
|
+
return puestas;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* El entorno del motor SIN las variables de claves de proveedores. Si llegan del
|
|
857
|
+
* entorno, el motor las da por puestas y NO deja editarlas en Ajustes › Models.
|
|
858
|
+
* La única fuente de claves es la casa (lo que se pega en Ajustes › Models).
|
|
859
|
+
*/
|
|
860
|
+
function entornoSinClaves(casa) {
|
|
861
|
+
const fuera = new Set(['B_AI_API_KEY', 'BAI_API_KEY', 'OPENROUTER_API_KEY', 'DEEPSEEK_API_KEY', 'GROQ_API_KEY',
|
|
862
|
+
'GEMINI_API_KEY', 'NVIDIA_API_KEY', 'SAMBANOVA_API_KEY', 'CLOUDFLARE_API_KEY', 'CLOUDFLARE_API_TOKEN']);
|
|
863
|
+
const ajustes = leerAjustes(casa) ?? {};
|
|
864
|
+
const nativa = ajustes?.['llm-deepseek']?.apiKeyEnv;
|
|
865
|
+
if (typeof nativa === 'string') fuera.add(nativa);
|
|
866
|
+
for (const perfil of Object.values(ajustes?.['llm-pi-ai']?.providers ?? {})) {
|
|
867
|
+
if (perfil && typeof perfil.apiKeyEnv === 'string') fuera.add(perfil.apiKeyEnv);
|
|
868
|
+
}
|
|
869
|
+
const env = { ...process.env, DSH_HOME: casa };
|
|
870
|
+
for (const k of Object.keys(env)) if (fuera.has(k.toUpperCase())) delete env[k];
|
|
871
|
+
return env;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// ── el encargo sin pantalla ─────────────────────────────────────────────────
|
|
875
|
+
function correrHeadless({ motor, casa, carpeta, perfilHeadless, encargo, modelo }) {
|
|
876
|
+
const parches = [];
|
|
877
|
+
let temporales = [];
|
|
878
|
+
if (modelo !== null) {
|
|
879
|
+
const ajustes = leerAjustes(casa) ?? {};
|
|
880
|
+
const porDefecto = ajustes?.['agent-default-model'];
|
|
881
|
+
const eleccion = resolverModelo(casa, modelo, porDefecto);
|
|
882
|
+
const { parche, copia } = parcheModelo(casa, eleccion);
|
|
883
|
+
parches.push(parche);
|
|
884
|
+
temporales = [copia, parche];
|
|
885
|
+
process.stdout.write('RATACODE · headless · modelo de este encargo: ' + eleccion.modelo
|
|
886
|
+
+ ' (proveedor ' + eleccion.proveedor + ')\n');
|
|
887
|
+
}
|
|
888
|
+
process.stdout.write('RATACODE · headless · ' + motor.version + ' · casa: ' + casa + '\n');
|
|
889
|
+
process.stdout.write('RATACODE · headless · carpeta de trabajo: ' + carpeta + '\n');
|
|
890
|
+
process.stdout.write('RATACODE · headless · perfil: ' + perfilHeadless + '\n');
|
|
891
|
+
|
|
892
|
+
const args = [motor.bin, '--profile', 'headless'];
|
|
893
|
+
for (const p of parches) args.push('--patch', p);
|
|
894
|
+
args.push(encargo);
|
|
895
|
+
const empezo = Date.now();
|
|
896
|
+
anotar(casa, 'RATACODE headless arranca · carpeta ' + carpeta);
|
|
897
|
+
const hijo = spawn(process.execPath, args, {
|
|
898
|
+
cwd: carpeta,
|
|
899
|
+
env: entornoSinClaves(casa),
|
|
900
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
901
|
+
windowsHide: true,
|
|
902
|
+
});
|
|
903
|
+
const limpiar = () => {
|
|
904
|
+
for (const ruta of temporales) {
|
|
905
|
+
try { rmSync(ruta, { force: true }); } catch { /* da igual: son temporales */ }
|
|
906
|
+
}
|
|
907
|
+
temporales = [];
|
|
908
|
+
};
|
|
909
|
+
hijo.on('error', (e) => {
|
|
910
|
+
process.stderr.write('RATACODE · el motor no arrancó: ' + e.message + '\n');
|
|
911
|
+
anotar(casa, 'el motor headless no arrancó · ' + e.message);
|
|
912
|
+
limpiar();
|
|
913
|
+
process.exitCode = 1;
|
|
914
|
+
});
|
|
915
|
+
hijo.on('exit', (codigo, senal) => {
|
|
916
|
+
anotar(casa, 'el motor headless se cerró · ' + cierre(codigo, senal)
|
|
917
|
+
+ ' · vivió ' + Math.round((Date.now() - empezo) / 1000) + ' s');
|
|
918
|
+
limpiar();
|
|
919
|
+
process.exitCode = typeof codigo === 'number' ? codigo : 1;
|
|
920
|
+
});
|
|
921
|
+
process.on('SIGINT', () => { anotar(casa, 'paro el motor headless (SIGINT)'); matarArbol(hijo); limpiar(); process.exit(130); });
|
|
922
|
+
process.on('SIGTERM', () => { anotar(casa, 'paro el motor headless (SIGTERM)'); matarArbol(hijo); limpiar(); process.exit(143); });
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ── el registro de la casa ──────────────────────────────────────────────────
|
|
926
|
+
/** La ruta del registro: `<casa>\ratacode.log`. */
|
|
927
|
+
function rutaDelLog(casa) {
|
|
928
|
+
return join(casa, 'ratacode.log');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Deja una línea en `<casa>\ratacode.log`. Sirve para saber POR QUÉ se cerró
|
|
933
|
+
* el motor: hasta hoy, una instancia que se caía sola no dejaba ni rastro.
|
|
934
|
+
* Si el registro no se puede escribir, eso NO para el motor.
|
|
935
|
+
*/
|
|
936
|
+
function anotar(casa, texto) {
|
|
937
|
+
try {
|
|
938
|
+
mkdirSync(casa, { recursive: true });
|
|
939
|
+
appendFileSync(rutaDelLog(casa), new Date().toISOString() + ' · ' + texto + '\n');
|
|
940
|
+
} catch { /* el registro es una ayuda, no un requisito */ }
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** El texto de un cierre, con código de salida y señal. */
|
|
944
|
+
function cierre(codigo, senal) {
|
|
945
|
+
// En Windows, un proceso matado a la fuerza no deja señal: deja 0xFFFFFFFF.
|
|
946
|
+
const nota = codigo === 4294967295 ? ' (0xFFFFFFFF: matado a la fuerza)' : '';
|
|
947
|
+
return 'código ' + (codigo === null || codigo === undefined ? '(ninguno)' : codigo) + nota
|
|
948
|
+
+ ' · señal ' + (senal === null || senal === undefined ? '(ninguna)' : senal);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// ── el panel ────────────────────────────────────────────────────────────────
|
|
952
|
+
/**
|
|
953
|
+
* Espera a que el puerto de la URL conteste DE VERDAD. DSH imprime la URL en
|
|
954
|
+
* cuanto monta el servidor, pero hasta que el puerto no escucha esa URL no
|
|
955
|
+
* abre: escribir `url.txt` antes deja un fichero que no lleva a ninguna parte.
|
|
956
|
+
*/
|
|
957
|
+
function esperarPuerto(url, plazoMs = 20000) {
|
|
958
|
+
return new Promise((listo) => {
|
|
959
|
+
let host;
|
|
960
|
+
let puerto;
|
|
961
|
+
try {
|
|
962
|
+
const u = new URL(url);
|
|
963
|
+
host = u.hostname === 'localhost' ? '127.0.0.1' : u.hostname.replace(/^\[|\]$/g, '');
|
|
964
|
+
puerto = Number(u.port || (u.protocol === 'https:' ? 443 : 80));
|
|
965
|
+
} catch {
|
|
966
|
+
listo(false);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (!Number.isInteger(puerto) || puerto <= 0) { listo(false); return; }
|
|
970
|
+
const limite = Date.now() + plazoMs;
|
|
971
|
+
const intento = () => {
|
|
972
|
+
const s = connect({ host, port: puerto });
|
|
973
|
+
s.setTimeout(1000);
|
|
974
|
+
const fallo = () => {
|
|
975
|
+
s.destroy();
|
|
976
|
+
if (Date.now() >= limite) { listo(false); return; }
|
|
977
|
+
setTimeout(intento, 150);
|
|
978
|
+
};
|
|
979
|
+
s.once('connect', () => { s.destroy(); listo(true); });
|
|
980
|
+
s.once('error', fallo);
|
|
981
|
+
s.once('timeout', fallo);
|
|
982
|
+
};
|
|
983
|
+
intento();
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function correrPanel({ motor, casa, carpeta, ordenes }) {
|
|
988
|
+
const parche = parcheSelectorCarpeta(casa);
|
|
989
|
+
const conClavesEnWindows = anotarVariablesDeClaves(casa);
|
|
990
|
+
const args = [motor.bin, '--profile', 'web', '--patch', parche, '--port', String(ordenes.puerto)];
|
|
991
|
+
if (!ordenes.abrir) args.push('--no-open');
|
|
992
|
+
const rutaUrl = join(casa, 'url.txt');
|
|
993
|
+
const empezo = Date.now();
|
|
994
|
+
anotar(casa, 'RATACODE arranca · puerto ' + ordenes.puerto + ' · carpeta ' + carpeta
|
|
995
|
+
+ (conClavesEnWindows.length > 0 ? ' · variables de claves en Windows: ' + conClavesEnWindows.join(', ') : ''));
|
|
996
|
+
if (conClavesEnWindows.length > 0) {
|
|
997
|
+
process.stdout.write('RATACODE · en Windows tienes ' + conClavesEnWindows.join(', ')
|
|
998
|
+
+ ': RATACODE ya no las usa (las claves se ponen en Ajustes › Models). El panel te lo recuerda una vez.\n');
|
|
999
|
+
}
|
|
1000
|
+
const hijo = spawn(process.execPath, args, {
|
|
1001
|
+
cwd: carpeta,
|
|
1002
|
+
env: entornoSinClaves(casa),
|
|
1003
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1004
|
+
windowsHide: true,
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
let visto = '';
|
|
1008
|
+
let anunciada = false;
|
|
1009
|
+
/** La URL está dicha y el puerto escucha: ahora sí se escribe url.txt. */
|
|
1010
|
+
const cuandoEscuche = async (url) => {
|
|
1011
|
+
const escucha = await esperarPuerto(url);
|
|
1012
|
+
if (hijo.exitCode !== null) return; // se cerró mientras mirábamos
|
|
1013
|
+
if (!escucha) {
|
|
1014
|
+
process.stderr.write('RATACODE · el motor dio su URL, pero el puerto ' + ordenes.puerto
|
|
1015
|
+
+ ' no contesta: NO escribo url.txt (' + url + ')\n');
|
|
1016
|
+
anotar(casa, 'el motor dio URL pero el puerto no contestó en 20 s: no se escribió url.txt');
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
mkdirSync(casa, { recursive: true });
|
|
1021
|
+
// La URL de la vez anterior se quita AQUÍ, no al arrancar: si esta
|
|
1022
|
+
// instancia no llega a escuchar, la del panel que ya estaba vivo no se
|
|
1023
|
+
// toca (antes, un segundo `ratacode` sobre la misma casa dejaba al primero
|
|
1024
|
+
// sin url.txt antes de saber siquiera si podría escuchar).
|
|
1025
|
+
try { rmSync(rutaUrl, { force: true }); } catch { /* no había nada que borrar */ }
|
|
1026
|
+
writeFileSync(rutaUrl, url + '\n');
|
|
1027
|
+
} catch (e) {
|
|
1028
|
+
process.stderr.write('RATACODE · no pude escribir ' + rutaUrl + ': ' + e.message + '\n');
|
|
1029
|
+
anotar(casa, 'no se pudo escribir url.txt · ' + e.message);
|
|
1030
|
+
}
|
|
1031
|
+
process.stdout.write('\nRATACODE · ABIERTO EN: ' + url + '\n');
|
|
1032
|
+
process.stdout.write('RATACODE · la URL (con su token) está guardada en: ' + rutaUrl + '\n\n');
|
|
1033
|
+
anotar(casa, 'el motor escucha en el puerto ' + ordenes.puerto + ': url.txt escrito');
|
|
1034
|
+
};
|
|
1035
|
+
const cazar = (trozo) => {
|
|
1036
|
+
if (anunciada) return;
|
|
1037
|
+
visto = (visto + trozo).slice(-8192);
|
|
1038
|
+
const m = /dsh web:\s+(\S+)/.exec(visto);
|
|
1039
|
+
if (!m) return;
|
|
1040
|
+
anunciada = true;
|
|
1041
|
+
cuandoEscuche(m[1]);
|
|
1042
|
+
};
|
|
1043
|
+
hijo.stdout.setEncoding('utf8');
|
|
1044
|
+
hijo.stdout.on('data', (t) => { process.stdout.write(t); cazar(t); });
|
|
1045
|
+
hijo.stderr.setEncoding('utf8');
|
|
1046
|
+
hijo.stderr.on('data', (t) => { process.stderr.write(t); cazar(t); });
|
|
1047
|
+
hijo.on('error', (e) => {
|
|
1048
|
+
process.stderr.write('RATACODE · el motor no arrancó: ' + e.message + '\n');
|
|
1049
|
+
anotar(casa, 'el motor no arrancó · ' + e.message);
|
|
1050
|
+
process.exitCode = 1;
|
|
1051
|
+
});
|
|
1052
|
+
hijo.on('exit', (codigo, senal) => {
|
|
1053
|
+
anotar(casa, 'el motor se cerró · ' + cierre(codigo, senal)
|
|
1054
|
+
+ ' · URL ' + (anunciada ? 'sí' : 'no')
|
|
1055
|
+
+ ' · vivió ' + Math.round((Date.now() - empezo) / 1000) + ' s');
|
|
1056
|
+
if (!anunciada) {
|
|
1057
|
+
process.stderr.write('RATACODE · el motor se cerró sin dar URL (' + cierre(codigo, senal) + '). '
|
|
1058
|
+
+ 'Si el puerto ' + ordenes.puerto + ' está ocupado, prueba: ratacode --port 3778\n');
|
|
1059
|
+
} else if (senal) {
|
|
1060
|
+
process.stderr.write('RATACODE · el motor se cerró por la señal ' + senal + '\n');
|
|
1061
|
+
}
|
|
1062
|
+
process.exitCode = typeof codigo === 'number' ? codigo : 1;
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
const fuera = (motivo) => {
|
|
1066
|
+
if (hijo.exitCode === null && !hijo.killed) anotar(casa, 'paro el motor (' + motivo + ')');
|
|
1067
|
+
matarArbol(hijo);
|
|
1068
|
+
};
|
|
1069
|
+
process.on('SIGINT', () => { fuera('SIGINT'); process.exit(130); });
|
|
1070
|
+
process.on('SIGTERM', () => { fuera('SIGTERM'); process.exit(143); });
|
|
1071
|
+
process.on('exit', (codigo) => { fuera('RATACODE se cierra, código ' + codigo); });
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* `ratacode mcp`: el servidor MCP, como subcomando del mismo binario. Se lanza
|
|
1076
|
+
* como hijo con la entrada y la salida HEREDADAS: por stdio, stdout es del
|
|
1077
|
+
* protocolo MCP y RATACODE no puede escribir ni una letra ahí.
|
|
1078
|
+
*
|
|
1079
|
+
* Antes de arrancarlo se estrena la casa (si es nueva) para que el MCP tenga
|
|
1080
|
+
* los mismos proveedores y modelos que la web, pero TODO lo que cuenta
|
|
1081
|
+
* RATACODE va a stderr.
|
|
1082
|
+
*/
|
|
1083
|
+
function correrMcp({ casa, argv }) {
|
|
1084
|
+
const bin = join(PAQUETE, 'mcp', 'bin', 'ratacode-mcp.js');
|
|
1085
|
+
if (!existsSync(bin)) {
|
|
1086
|
+
process.stderr.write('RATACODE · no encuentro el servidor MCP en ' + bin
|
|
1087
|
+
+ ' (¿instalación sin la carpeta mcp?)\n');
|
|
1088
|
+
process.exitCode = 1;
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
// `--help` y `--status` no arrancan nada: no se toca la casa (estrenarla en un
|
|
1092
|
+
// «--help» sería un efecto raro, y encima fuera de donde el usuario mira).
|
|
1093
|
+
const soloMira = argv.some((a) => a === '-h' || a === '--help' || a === '--status');
|
|
1094
|
+
if (!soloMira) {
|
|
1095
|
+
const estreno = estrenarCasa(casa, process.env);
|
|
1096
|
+
// Los 9 modos, también en una casa que ya existía: el MCP compone sesiones
|
|
1097
|
+
// por el mismo camino que el panel y necesita el mismo roster.
|
|
1098
|
+
const modos = asegurarModos(casa);
|
|
1099
|
+
process.stderr.write('RATACODE · casa ' + (estreno.nueva ? 'NUEVA estrenada: ' : 'ya existía: ') + casa
|
|
1100
|
+
+ ' · modos por defecto: ' + PRESET_POR_DEFECTO
|
|
1101
|
+
+ (modos.borrados.length > 0 ? ' · fuera los viejos: ' + modos.borrados.join(', ') : '') + '\n');
|
|
1102
|
+
}
|
|
1103
|
+
const hijo = spawn(process.execPath, [bin, ...argv], { env: entornoSinClaves(casa), stdio: 'inherit', windowsHide: true });
|
|
1104
|
+
hijo.on('error', (e) => {
|
|
1105
|
+
process.stderr.write('RATACODE · el MCP no arrancó: ' + e.message + '\n');
|
|
1106
|
+
process.exitCode = 1;
|
|
1107
|
+
});
|
|
1108
|
+
hijo.on('exit', (codigo) => { process.exitCode = typeof codigo === 'number' ? codigo : 1; });
|
|
1109
|
+
const fuera = () => { matarArbol(hijo); };
|
|
1110
|
+
process.on('SIGINT', () => { fuera(); process.exit(130); });
|
|
1111
|
+
process.on('SIGTERM', () => { fuera(); process.exit(143); });
|
|
1112
|
+
process.on('exit', fuera);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
async function main() {
|
|
1116
|
+
exigirNode();
|
|
1117
|
+
const ordenes = leerOrdenes(process.argv.slice(2));
|
|
1118
|
+
if (ordenes.ayuda) {
|
|
1119
|
+
process.stdout.write(uso());
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const casa = ordenes.casa ?? resolve(process.env.RATACODE_HOME || join(homedir(), '.ratacode'));
|
|
1123
|
+
if (ordenes.modo === 'mcp') {
|
|
1124
|
+
correrMcp({ casa, argv: ordenes.mcpArgv });
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const perfilWeb = join(casa, 'profiles', 'web');
|
|
1128
|
+
const perfilHeadless = join(casa, 'profiles', 'headless');
|
|
1129
|
+
const motor = binDelMotor();
|
|
1130
|
+
|
|
1131
|
+
const cuenta = copiarArbol(PIEL_ORIGEN, join(perfilWeb, 'node_modules', NOMBRE_PLUGIN), { copiados: 0, iguales: 0 });
|
|
1132
|
+
// El texto de la conexión viaja CON el plugin: la piel lo sirve en
|
|
1133
|
+
// /ratacode/handshake (Ajustes > Conexiones) y, copiada dentro del perfil, no
|
|
1134
|
+
// tiene el repositorio al lado. Se copia en <plugin>\apreton\.
|
|
1135
|
+
const cuentaApreton = copiarArbol(APRETON_ORIGEN, join(perfilWeb, 'node_modules', NOMBRE_PLUGIN, 'apreton'), { copiados: 0, iguales: 0 });
|
|
1136
|
+
// Y la ruta de ESTA instalación (la carpeta del paquete, con `mcp\tunel.mjs`
|
|
1137
|
+
// dentro), para que los dos comandos del MCP que enseña Ajustes > Conexiones
|
|
1138
|
+
// lleven la ruta de verdad y no un «<ruta>» que el usuario tenga que buscar.
|
|
1139
|
+
writeFileSync(join(perfilWeb, 'node_modules', NOMBRE_PLUGIN, 'instalacion.txt'), PAQUETE + '\n');
|
|
1140
|
+
const capaWeb = prepararPerfil(perfilWeb, 'dsh-profile-web',
|
|
1141
|
+
['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', NOMBRE_PLUGIN], 'live');
|
|
1142
|
+
prepararPerfil(perfilHeadless, 'dsh-profile-headless',
|
|
1143
|
+
['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-headless'], 'startup');
|
|
1144
|
+
const estreno = estrenarCasa(casa, process.env);
|
|
1145
|
+
// Los 9 modos se copian SIEMPRE, también en una casa que ya existía: los de
|
|
1146
|
+
// versiones anteriores salieron con dos campos obligatorios sin poner y NO
|
|
1147
|
+
// cargaban («invalid config: - $.x missing required value»). `asegurarModos`
|
|
1148
|
+
// sólo escribe lo que cambia, borra los presets de RATACODE que ya no existen
|
|
1149
|
+
// (enlazador, promptista, escritor) y deja `agent-presets.default` en
|
|
1150
|
+
// modo-rata; los presets del USUARIO no se rozan.
|
|
1151
|
+
const modos = asegurarModos(casa);
|
|
1152
|
+
// R21 · el español de la casa: se apunta `locale.preference: es` si la casa no
|
|
1153
|
+
// ha elegido ya otro idioma (el inglés y el chino siguen en Ajustes › General ›
|
|
1154
|
+
// Language, y la elección del usuario se respeta siempre).
|
|
1155
|
+
const idioma = ponerIdiomaPorDefecto(casa);
|
|
1156
|
+
// R21 · el aspecto: se apunta el tema de fábrica (RATACODE PINK) si la casa no
|
|
1157
|
+
// tiene ninguno apuntado. La elección del usuario no se toca.
|
|
1158
|
+
const aspecto = ponerTemaPorDefecto(casa);
|
|
1159
|
+
|
|
1160
|
+
const carpeta = exigirCarpeta(ordenes.carpeta ?? process.cwd());
|
|
1161
|
+
const espacio = registrarEspacio(casa, carpeta);
|
|
1162
|
+
|
|
1163
|
+
process.stdout.write('RATACODE · ' + (motor.version ? 'v' + motor.version : '')
|
|
1164
|
+
+ (ordenes.modo === 'headless' ? ' · headless' : ' · puerto ' + ordenes.puerto) + '\n');
|
|
1165
|
+
process.stdout.write('RATACODE · casa: ' + casa + (estreno.nueva ? ' (NUEVA)' : ' (ya existía)')
|
|
1166
|
+
+ ' · aviso Internal Testing: ' + estreno.aviso + '\n');
|
|
1167
|
+
process.stdout.write('RATACODE · espacio de trabajo: ' + carpeta
|
|
1168
|
+
+ (espacio.registrado
|
|
1169
|
+
? (espacio.nuevo ? ' (registrado en el registro de espacios)' : ' (ya estaba registrado)')
|
|
1170
|
+
: ' (NO registrado: ' + espacio.motivo + ')') + '\n');
|
|
1171
|
+
process.stdout.write('RATACODE · modelo por defecto: ' + (estreno.modelo
|
|
1172
|
+
? estreno.modelo.modelo + ' (proveedor ' + estreno.modelo.proveedor + ', porque existe '
|
|
1173
|
+
+ estreno.modelo.variable + ' en el entorno)'
|
|
1174
|
+
: (estreno.nueva
|
|
1175
|
+
? 'el de fábrica (ninguna clave en el entorno: B_AI_API_KEY, OPENROUTER_API_KEY ni DEEPSEEK_API_KEY)'
|
|
1176
|
+
: 'el que ya tuviera la casa (no se toca settings.yaml)')) + '\n');
|
|
1177
|
+
process.stdout.write('RATACODE · piel: ' + cuenta.copiados + ' fichero(s) puesto(s), ' + cuenta.iguales + ' ya estaban igual'
|
|
1178
|
+
+ ' · conexiones: ' + cuentaApreton.copiados + ' puesto(s), ' + cuentaApreton.iguales + ' igual\n');
|
|
1179
|
+
process.stdout.write('RATACODE · modos: ' + modos.cuenta.copiados + ' fichero(s) puesto(s), ' + modos.cuenta.iguales
|
|
1180
|
+
+ ' ya estaban igual en ' + modos.presets
|
|
1181
|
+
+ (modos.borrados.length > 0 ? ' · fuera los viejos: ' + modos.borrados.join(', ') : '')
|
|
1182
|
+
+ ' · por defecto: ' + (modos.preset.cambiado ? 'puesto en ' + PRESET_POR_DEFECTO + ' (' + modos.preset.motivo + ')' : modos.preset.motivo) + '\n');
|
|
1183
|
+
if (capaWeb.reparado) {
|
|
1184
|
+
process.stdout.write('RATACODE · tu perfil tenía el parche roto (un `[]` pegado delante de las filas, de la 0.2.0):'
|
|
1185
|
+
+ ' reparado · ' + capaWeb.motivo + '\n');
|
|
1186
|
+
}
|
|
1187
|
+
process.stdout.write('RATACODE · proveedores: ' + (estreno.nueva
|
|
1188
|
+
? 'Ajustes › Models: las 8 APIs con clave (B.AI, OpenRouter, Groq, Google Gemini, NVIDIA NIM, SambaNova, Cloudflare Workers AI y DeepSeek nativo) · Ajustes › Modelos locales: Ollama y LM Studio, sin clave'
|
|
1189
|
+
: 'los que ya tuviera la casa (no se toca settings.yaml): añade a mano los que falten de las 8 APIs') + '\n');
|
|
1190
|
+
process.stdout.write('RATACODE · manos: el texto de la conexión y el MCP viven en Ajustes › Conexiones\n');
|
|
1191
|
+
process.stdout.write('RATACODE · idioma: ' + (idioma.cambiado
|
|
1192
|
+
? 'español puesto por defecto (' + idioma.motivo + ')'
|
|
1193
|
+
: 'el que ya tuviera la casa (' + idioma.motivo + ')')
|
|
1194
|
+
+ ' · inglés y chino siguen en Ajustes › General › Language\n');
|
|
1195
|
+
process.stdout.write('RATACODE · aspecto: ' + (aspecto.cambiado
|
|
1196
|
+
? 'RATACODE PINK puesto por defecto (' + aspecto.motivo + ')'
|
|
1197
|
+
: 'el que ya tuviera la casa (' + aspecto.motivo + ')')
|
|
1198
|
+
+ ' · los tres (PINK, YELLOW, MINIMAL) en Ajustes › General › Aspecto\n');
|
|
1199
|
+
|
|
1200
|
+
if (ordenes.modo === 'headless') {
|
|
1201
|
+
// Antes de arrancar el motor, mira si hay con qué: si el modelo por defecto
|
|
1202
|
+
// apunta a un proveedor sin clave por ningún lado, el motor falla en inglés
|
|
1203
|
+
// y con tripas (`MISSING_CREDENTIAL: llm-pi-ai: no credential for provider
|
|
1204
|
+
// route "b-ai"…`). Mejor dos líneas en español y no arrancar nada.
|
|
1205
|
+
const ajustesCasa = leerAjustes(casa) ?? {};
|
|
1206
|
+
const porDefecto = ajustesCasa['agent-default-model'] ?? {};
|
|
1207
|
+
const eleccion = ordenes.modelo === null
|
|
1208
|
+
? { proveedor: porDefecto.provider, modelo: porDefecto.model }
|
|
1209
|
+
: resolverModelo(casa, ordenes.modelo, porDefecto);
|
|
1210
|
+
if (typeof eleccion.proveedor === 'string' && eleccion.proveedor !== '') {
|
|
1211
|
+
const credencial = credencialDe(casa, ajustesCasa, eleccion.proveedor);
|
|
1212
|
+
if (credencial !== null && !credencial.esta && !credencial.enLaCasa) {
|
|
1213
|
+
process.stderr.write([
|
|
1214
|
+
'',
|
|
1215
|
+
'RATACODE · PARA.',
|
|
1216
|
+
'',
|
|
1217
|
+
' El modelo «' + (eleccion.modelo ?? '(sin nombre)') + '» va por el proveedor «' + eleccion.proveedor
|
|
1218
|
+
+ '», y no encuentro su clave (' + credencial.nombre + ').',
|
|
1219
|
+
' Ponla en la web (Ajustes → Models: queda en ' + join(casa, '.credentials.yaml')
|
|
1220
|
+
+ ') o expórtala: ' + credencial.nombre + '=... (y vuelve a lanzarlo).',
|
|
1221
|
+
'',
|
|
1222
|
+
].join('\n'));
|
|
1223
|
+
process.exitCode = 1;
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
correrHeadless({ motor, casa, carpeta, perfilHeadless, encargo: ordenes.encargo, modelo: ordenes.modelo });
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
// El puerto, ANTES de arrancar el motor: si está ocupado, el fallo crudo del
|
|
1231
|
+
// motor (EADDRINUSE en inglés, 45 líneas) no le sirve a nadie.
|
|
1232
|
+
if (ordenes.puerto !== 0 && await puertoOcupado(ordenes.puerto)) {
|
|
1233
|
+
process.stderr.write([
|
|
1234
|
+
'',
|
|
1235
|
+
'RATACODE · PARA.',
|
|
1236
|
+
'',
|
|
1237
|
+
' El puerto ' + ordenes.puerto + ' ya está ocupado (¿tienes otro RATACODE abierto?).',
|
|
1238
|
+
' Prueba con otro: ratacode --port ' + (ordenes.puerto === 3778 ? 3779 : 3778),
|
|
1239
|
+
' O mira quién lo tiene: Get-NetTCPConnection -LocalPort ' + ordenes.puerto,
|
|
1240
|
+
'',
|
|
1241
|
+
].join('\n'));
|
|
1242
|
+
process.exitCode = 1;
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
correrPanel({ motor, casa, carpeta, ordenes });
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
main().catch((e) => {
|
|
1249
|
+
process.stderr.write('RATACODE · ' + (e && e.message ? e.message : String(e)) + '\n');
|
|
1250
|
+
process.exitCode = 1;
|
|
1251
|
+
});
|