@orxataguy/tyr 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.
@@ -0,0 +1,162 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import axios from 'axios';
4
+ import dotenv from 'dotenv';
5
+ import type { TyrContext } from '../Kernel';
6
+
7
+ type AIProvider = 'claude' | 'openai' | 'gemini';
8
+
9
+ interface AIConfig {
10
+ provider: AIProvider;
11
+ apiKey: string;
12
+ }
13
+
14
+ const PROVIDERS: { env: string; provider: AIProvider }[] = [
15
+ { env: 'CLAUDE_API_KEY', provider: 'claude' },
16
+ { env: 'OPENAI_API_KEY', provider: 'openai' },
17
+ { env: 'GEMINI_API_KEY', provider: 'gemini' },
18
+ ];
19
+
20
+ function detectProvider(): AIConfig | null {
21
+ for (const { env, provider } of PROVIDERS) {
22
+ const key = process.env[env];
23
+ if (key) return { provider, apiKey: key };
24
+ }
25
+ return null;
26
+ }
27
+
28
+ async function callAI(config: AIConfig, sys: string, user: string): Promise<string> {
29
+ switch (config.provider) {
30
+ case 'claude': {
31
+ const res = await axios.post('https://api.anthropic.com/v1/messages', {
32
+ model: 'claude-sonnet-4-20250514',
33
+ max_tokens: 4096,
34
+ system: sys,
35
+ messages: [{ role: 'user', content: user }],
36
+ }, {
37
+ headers: {
38
+ 'x-api-key': config.apiKey,
39
+ 'anthropic-version': '2023-06-01',
40
+ 'content-type': 'application/json',
41
+ },
42
+ });
43
+ return res.data.content[0].text;
44
+ }
45
+ case 'openai': {
46
+ const res = await axios.post('https://api.openai.com/v1/chat/completions', {
47
+ model: 'gpt-4o',
48
+ messages: [
49
+ { role: 'system', content: sys },
50
+ { role: 'user', content: user },
51
+ ],
52
+ max_tokens: 4096,
53
+ }, {
54
+ headers: {
55
+ 'Authorization': `Bearer ${config.apiKey}`,
56
+ 'content-type': 'application/json',
57
+ },
58
+ });
59
+ return res.data.choices[0].message.content;
60
+ }
61
+ case 'gemini': {
62
+ const res = await axios.post(
63
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${config.apiKey}`,
64
+ {
65
+ system_instruction: { parts: [{ text: sys }] },
66
+ contents: [{ parts: [{ text: user }] }],
67
+ },
68
+ { headers: { 'content-type': 'application/json' } }
69
+ );
70
+ return res.data.candidates[0].content.parts[0].text;
71
+ }
72
+ }
73
+ }
74
+
75
+ function extractCodeBlock(response: string): string {
76
+ const match = response.match(/```(?:typescript|ts)?\s*\n([\s\S]*?)```/);
77
+ return match ? match[1].trim() : response.trim();
78
+ }
79
+
80
+ function buildSystemPrompt(frameworkRoot: string): string {
81
+ const libPath = path.resolve(frameworkRoot, 'src/lib');
82
+ let mods = '';
83
+
84
+ if (fs.existsSync(libPath)) {
85
+ for (const file of fs.readdirSync(libPath).filter(f => f.endsWith('.ts'))) {
86
+ const content = fs.readFileSync(path.join(libPath, file), 'utf8');
87
+ const methods: string[] = [];
88
+ const re = /\/\*\*([\s\S]*?)\*\/\s*(public\s+(?:async\s+)?(\w+))?/g;
89
+ let m;
90
+ while ((m = re.exec(content)) !== null) {
91
+ if (m[3]) {
92
+ const desc = m[1].replace(/\*/g, '').replace(/@\w+\s*/g, '').trim().split('\n')[0].trim();
93
+ methods.push(`${m[3]}:${desc}`);
94
+ }
95
+ }
96
+ if (methods.length) mods += `\n${file.replace('.ts', '')}:${methods.join(';')}\n`;
97
+ }
98
+ }
99
+
100
+ return `Genera comando Tyr (TS CLI).
101
+
102
+ FORMATO:
103
+ import{TyrContext}from'../core/Kernel';
104
+ export default({run,task,fail,logger,...mgrs}:TyrContext)=>{
105
+ return async(args:string[])=>{/*impl*/};};
106
+ export const Test={args:['ej1','ej2']};
107
+
108
+ KERNEL:run(cmd,args),task(desc,fn),fail(msg,hint?),logger:{info,success,warn,error}
109
+ MANAGERS(destructurar):shell(exec,cd,input,showLoader),fs(read,write,exists,delete),git(clone),docker,pkg,db,web,sys${mods}
110
+
111
+ REGLAS:export default;async(args:string[]);task() p/errores;fail() p/validar;Test con args realistas.
112
+ Responde SOLO código TS sin explicaciones ni backticks.`;
113
+ }
114
+
115
+ export default function ai({ logger, fs: tyrFs, frameworkRoot, run, fail }: TyrContext) {
116
+ return async (args: string[]) => {
117
+ const commandName = args[0];
118
+ const prompt = args.slice(1).join(' ');
119
+
120
+ if (!commandName || !prompt) {
121
+ return fail(
122
+ "Uso incorrecto de ai.",
123
+ "Sintaxis: tyr ai [nombre-comando] [prompt]"
124
+ );
125
+ }
126
+
127
+ dotenv.config({ path: path.resolve(frameworkRoot, '.env'), override: true });
128
+ const aiConfig = detectProvider();
129
+ if (!aiConfig) {
130
+ return fail(
131
+ "No se encontró API key de IA.",
132
+ "Configura CLAUDE_API_KEY, OPENAI_API_KEY o GEMINI_API_KEY en .env"
133
+ );
134
+ }
135
+
136
+ logger.success(`API: ${aiConfig.provider}`);
137
+
138
+ logger.info(`Scaffold '${commandName}'...`);
139
+ await run('gen', [commandName, commandName]);
140
+
141
+ const systemPrompt = buildSystemPrompt(frameworkRoot);
142
+
143
+ logger.info(`Enviando a ${aiConfig.provider}...`);
144
+
145
+ let code: string;
146
+ try {
147
+ const response = await callAI(aiConfig, systemPrompt, prompt);
148
+ code = extractCodeBlock(response);
149
+ logger.success(`OK (${code.length} chars)`);
150
+ } catch (e: any) {
151
+ const msg = e.response?.data?.error?.message || e.message;
152
+ return fail(
153
+ `Error ${aiConfig.provider}: ${msg}`,
154
+ `'${commandName}' creado con template base. Revisa tu API key.`
155
+ );
156
+ }
157
+
158
+ const filePath = path.resolve(frameworkRoot, 'src/commands', `${commandName}.tyr.ts`);
159
+ await tyrFs.write(filePath, code);
160
+ logger.success(`'${commandName}' -> src/commands/${commandName}.tyr.ts`);
161
+ };
162
+ }
@@ -0,0 +1,325 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import http from 'http';
4
+ import { TyrContext } from '../Kernel';
5
+
6
+ interface DocMethod {
7
+ name: string;
8
+ description: string;
9
+ example: string | null;
10
+ }
11
+
12
+ interface DocStructure {
13
+ name: string;
14
+ description: string;
15
+ methods: DocMethod[];
16
+ }
17
+
18
+ export default function doc({ logger, frameworkRoot, run }: TyrContext) {
19
+ return async (args: string[]) => {
20
+ logger.info("📚 Generando documentación del sistema (TS Mode)...");
21
+
22
+ const libPath = path.resolve(frameworkRoot, 'src/lib');
23
+
24
+ const parseJSDoc = (filename: string, content: string): DocStructure => {
25
+ const fileDoc: DocStructure = {
26
+ name: filename,
27
+ description: "Sin descripción.",
28
+ methods: []
29
+ };
30
+
31
+ const cleanJSDoc = (raw: string) => {
32
+ return raw
33
+ .split('\n')
34
+ .map(line => {
35
+ return line.trim().replace(/^\*+\s?/, '');
36
+ })
37
+ .filter(line => line !== '')
38
+ .join('\n');
39
+ };
40
+
41
+ const commentRegex = /\/\*\*([\s\S]*?)\*\//g;
42
+ let match;
43
+
44
+ while ((match = commentRegex.exec(content)) !== null) {
45
+ const rawComment = match[1];
46
+ const cleanComment = cleanJSDoc(rawComment);
47
+
48
+ const nextCodeIndex = commentRegex.lastIndex;
49
+ const codeSnippet = content.substring(nextCodeIndex, nextCodeIndex + 200);
50
+
51
+ const isClass = /@class/.test(cleanComment) || /^\s*export\s+class/.test(codeSnippet);
52
+
53
+ if (isClass) {
54
+ const descMatch = cleanComment.match(/@description\s+([\s\S]*?)(?=@|$)/i);
55
+ if (descMatch) {
56
+ fileDoc.description = descMatch[1].trim();
57
+ } else {
58
+ fileDoc.description = cleanComment.split('\n')[0];
59
+ }
60
+ const classNameMatch = codeSnippet.match(/class\s+(\w+)/);
61
+ if (classNameMatch) fileDoc.name = classNameMatch[1];
62
+ continue;
63
+ }
64
+
65
+ let methodName = null;
66
+
67
+ const methodTagMatch = cleanComment.match(/@method\s+(\w+)/i);
68
+ if (methodTagMatch) {
69
+ methodName = methodTagMatch[1];
70
+ } else if (!codeSnippet.match(/^\s*constructor/)) {
71
+ const codeMatch = codeSnippet.match(/(?:public|private|protected)\s+(?:async\s+)?(\w+)/);
72
+ if (codeMatch) {
73
+ methodName = codeMatch[1];
74
+ }
75
+ }
76
+
77
+ if (methodName) {
78
+ let description = "";
79
+ const descMatch = cleanComment.match(/@description\s+([\s\S]*?)(?=@|$)/i);
80
+ if (descMatch) {
81
+ description = descMatch[1].trim();
82
+ } else {
83
+ const textLines = cleanComment.split('\n').filter(l => !l.startsWith('@'));
84
+ description = textLines.join(' ').trim() || "Sin descripción";
85
+ }
86
+
87
+ let example = null;
88
+ const exampleMatch = cleanComment.match(/@example([\s\S]*?)(?=@|$)/i);
89
+ if (exampleMatch) {
90
+ example = exampleMatch[1]
91
+ .replace(/```ts|```/g, '')
92
+ .trim();
93
+ }
94
+
95
+ fileDoc.methods.push({
96
+ name: methodName,
97
+ description: description,
98
+ example: example
99
+ });
100
+ }
101
+ }
102
+
103
+ return fileDoc;
104
+ };
105
+
106
+ if (!fs.existsSync(libPath)) {
107
+ logger.error(`No se encuentra la carpeta de librerías: ${libPath}`);
108
+ return;
109
+ }
110
+
111
+ const files = fs.readdirSync(libPath).filter(f => f.endsWith('.ts'));
112
+
113
+ if (files.length === 0) {
114
+ logger.warn("No se encontraron archivos .ts en /src/lib para documentar.");
115
+ }
116
+
117
+ const fileDocs = files.map(file => {
118
+ return parseJSDoc(file, fs.readFileSync(path.join(libPath, file), 'utf8'));
119
+ });
120
+
121
+ const systemDocs: DocStructure = {
122
+ name: 'TyrContext (Kernel)',
123
+ description: 'Utilidades globales inyectadas en cada comando. Accesibles destructurando el contexto.',
124
+ methods: [
125
+ {
126
+ name: 'run',
127
+ description: 'Ejecuta otro comando del sistema programáticamente (Composición de comandos). Útil para que un comando invoque a otros.',
128
+ example: `
129
+ // Llama al comando 'test' pasándole argumentos adicionales
130
+ const secret = "123";
131
+ args.push(secret);
132
+ await run('test', args);`.trim()
133
+ },
134
+ {
135
+ name: 'task',
136
+ description: 'Helper que envuelve una operación crítica. Si falla, el framework captura el error, añade contexto y lo muestra limpio en consola. Elimina la necesidad de try/catch manuales.',
137
+ example: `
138
+ // Ejemplo: Tarea asíncrona que retorna un valor
139
+ const buildId = await task('Compilando proyecto', async () => {
140
+ return await shell.exec('npm run build');
141
+ });
142
+
143
+ // Si falla, el log dirá: "Falló la tarea: Compilando proyecto"`.trim()
144
+ },
145
+ {
146
+ name: 'fail',
147
+ description: 'Detiene la ejecución del comando inmediatamente lanzando un error controlado. Permite añadir una "sugerencia" para ayudar al usuario a solucionarlo.',
148
+ example: `
149
+ // Úsalo para validaciones lógicas
150
+ if (!fs.existsSync('./package.json')) {
151
+ fail(
152
+ 'No se encuentra el archivo package de npm',
153
+ 'Ejecuta "npm init -y" para generar uno.'
154
+ );
155
+ }`.trim()
156
+ },
157
+ {
158
+ name: 'logger',
159
+ description: 'Sistema de logs estandarizado con colores y formatos.',
160
+ example: `logger.info('Iniciando...');\nlogger.success('Creado');\nlogger.warn('Cuidado');`
161
+ }
162
+ ]
163
+ };
164
+
165
+ const docs = [systemDocs, ...fileDocs];
166
+
167
+ const html = `
168
+ <!DOCTYPE html>
169
+ <html>
170
+ <head>
171
+ <meta charset="utf-8">
172
+ <title>Tyr Docs</title>
173
+ <style>
174
+ body { font-family: 'Segoe UI', sans-serif; background: #222; color: #eee; padding: 20px; display: flex; margin: 0; }
175
+ nav { width: 220px; border-right: 1px solid #444; margin-right: 20px; padding-right: 20px; height: 100vh; overflow-y: auto; position: sticky; top: 0; }
176
+ a { color: #4db8ff; text-decoration: none; display: block; margin: 8px 0; padding: 5px; border-radius: 4px; transition: 0.2s; }
177
+ a:hover { background: #333; }
178
+ main { flex: 1; overflow-y: auto; }
179
+ .card { background: #2d2d2d; padding: 20px; margin-bottom: 30px; border-radius: 8px; border: 1px solid #333; }
180
+ h2 { border-bottom: 1px solid #444; padding-bottom: 10px; margin-top: 0; color: #fff; }
181
+ .method { margin-top: 25px; padding-left: 15px; border-left: 3px solid #4db8ff; }
182
+ h3 { margin: 0 0 5px 0; color: #4db8ff; font-family: monospace; font-size: 1.2em; }
183
+ .desc { color: #ccc; margin-bottom: 10px; }
184
+ pre { background: #1a1a1a; padding: 15px; border-radius: 5px; overflow-x: auto; border: 1px solid #444; color: #ce9178; font-family: monospace; white-space: pre-wrap; }
185
+ .tag-ts { background: #007acc; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.7em; margin-left: 10px; vertical-align: middle; }
186
+ .prompt-box { background: #1a1a1a; border: 2px solid #4db8ff; padding: 25px; border-radius: 8px; margin-top: 40px; position: relative; }
187
+ .prompt-box h2 { color: #4db8ff; margin-top: 0; border: none; }
188
+ .copy-btn { position: absolute; top: 20px; right: 20px; background: #4db8ff; color: #000; border: none; padding: 8px 16px; border-radius: 5px; cursor: pointer; font-weight: bold; transition: 0.2s; }
189
+ .copy-btn:hover { background: #6dc9ff; }
190
+ .copy-btn:active { background: #2da3e0; }
191
+ </style>
192
+ </head>
193
+ <body>
194
+ <nav>
195
+ <h3 style="color: #888; text-transform: uppercase; font-size: 0.8rem;">Módulos TS</h3>
196
+ ${docs.map(d => `<a href="#${d.name}">📦 ${d.name.replace('.ts', '')}</a>`).join('')}
197
+ <a href="#ai-generator" style="margin-top: 20px; background: #4db8ff; color: #000; font-weight: bold;">🤖 Generador IA</a>
198
+ </nav>
199
+ <main>
200
+ ${docs.map(d => `
201
+ <div id="${d.name}" class="card">
202
+ <h2>${d.name} <span class="tag-ts">TS</span></h2>
203
+ <p style="font-size: 1.1em; color: #bbb;">${d.description}</p>
204
+ ${d.methods.map(m => `
205
+ <div class="method">
206
+ <h3>${m.name}()</h3>
207
+ <p class="desc">${m.description}</p>
208
+ ${m.example ? `<pre>${m.example}</pre>` : ''}
209
+ </div>
210
+ `).join('')}
211
+ </div>
212
+ `).join('')}
213
+
214
+ <div id="ai-generator" class="prompt-box">
215
+ <h2>🤖 Generador de Comandos con IA</h2>
216
+ <p style="color: #bbb; margin-bottom: 20px;">
217
+ Describe qué debe hacer tu comando y la IA lo generará automáticamente usando la documentación del framework.
218
+ </p>
219
+ <div style="margin-bottom: 15px;">
220
+ <label style="display: block; color: #4db8ff; margin-bottom: 5px; font-weight: bold;">Nombre del comando</label>
221
+ <input type="text" id="cmd-name" placeholder="mi-comando"
222
+ style="width: 100%; padding: 10px; background: #2d2d2d; border: 1px solid #444; color: #eee; border-radius: 5px; font-size: 1em; box-sizing: border-box;" />
223
+ </div>
224
+ <div style="margin-bottom: 15px;">
225
+ <label style="display: block; color: #4db8ff; margin-bottom: 5px; font-weight: bold;">Describe qué debe hacer el comando</label>
226
+ <textarea id="cmd-prompt" rows="6" placeholder="Ej: Crea un comando que liste todos los contenedores Docker activos y muestre su estado en una tabla formateada..."
227
+ style="width: 100%; padding: 10px; background: #2d2d2d; border: 1px solid #444; color: #eee; border-radius: 5px; font-size: 1em; resize: vertical; box-sizing: border-box;"></textarea>
228
+ </div>
229
+ <button id="generate-btn" onclick="generateCommand()"
230
+ style="background: #4db8ff; color: #000; border: none; padding: 12px 24px; border-radius: 5px; cursor: pointer; font-weight: bold; font-size: 1em; transition: 0.2s; width: 100%;">
231
+ Generar Comando
232
+ </button>
233
+ <div id="gen-status" style="margin-top: 15px; display: none; padding: 15px; border-radius: 5px;"></div>
234
+ </div>
235
+ </main>
236
+ <script>
237
+ async function generateCommand() {
238
+ const name = document.getElementById('cmd-name').value.trim();
239
+ const prompt = document.getElementById('cmd-prompt').value.trim();
240
+ const btn = document.getElementById('generate-btn');
241
+ const status = document.getElementById('gen-status');
242
+
243
+ if (!name || !prompt) {
244
+ status.style.display = 'block';
245
+ status.style.background = '#4a2020';
246
+ status.style.border = '1px solid #ff4444';
247
+ status.textContent = 'Rellena ambos campos.';
248
+ return;
249
+ }
250
+
251
+ btn.disabled = true;
252
+ btn.textContent = 'Generando...';
253
+ btn.style.background = '#888';
254
+ status.style.display = 'block';
255
+ status.style.background = '#1a1a2e';
256
+ status.style.border = '1px solid #4db8ff';
257
+ status.textContent = 'Enviando prompt a la IA... Esto puede tardar unos segundos.';
258
+
259
+ try {
260
+ const res = await fetch('/generate', {
261
+ method: 'POST',
262
+ headers: { 'Content-Type': 'application/json' },
263
+ body: JSON.stringify({ name, prompt })
264
+ });
265
+ const data = await res.json();
266
+
267
+ if (data.success) {
268
+ status.style.background = '#1a2e1a';
269
+ status.style.border = '1px solid #4ade80';
270
+ status.textContent = data.message;
271
+ } else {
272
+ status.style.background = '#4a2020';
273
+ status.style.border = '1px solid #ff4444';
274
+ status.textContent = data.message;
275
+ }
276
+ } catch (e) {
277
+ status.style.background = '#4a2020';
278
+ status.style.border = '1px solid #ff4444';
279
+ status.textContent = 'Error de conexión con el servidor.';
280
+ }
281
+
282
+ btn.disabled = false;
283
+ btn.textContent = 'Generar Comando';
284
+ btn.style.background = '#4db8ff';
285
+ }
286
+ </script>
287
+ </body>
288
+ </html>`;
289
+
290
+ const PORT = 3000;
291
+ const server = http.createServer(async (req, res) => {
292
+ if (req.method === 'POST' && req.url === '/generate') {
293
+ let body = '';
294
+ req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
295
+ req.on('end', async () => {
296
+ try {
297
+ const { name, prompt } = JSON.parse(body);
298
+ if (!name || !prompt) {
299
+ res.writeHead(400, { 'Content-Type': 'application/json' });
300
+ res.end(JSON.stringify({ success: false, message: 'Faltan campos obligatorios.' }));
301
+ return;
302
+ }
303
+
304
+ await run('ai', [name, prompt]);
305
+
306
+ res.writeHead(200, { 'Content-Type': 'application/json' });
307
+ res.end(JSON.stringify({ success: true, message: `Comando '${name}' generado correctamente en src/commands/${name}.tyr.ts` }));
308
+ } catch (e: any) {
309
+ res.writeHead(500, { 'Content-Type': 'application/json' });
310
+ res.end(JSON.stringify({ success: false, message: e.message || 'Error al generar el comando.' }));
311
+ }
312
+ });
313
+ return;
314
+ }
315
+
316
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
317
+ res.end(html);
318
+ });
319
+
320
+ server.listen(PORT, () => {
321
+ logger.success(`Documentación TS lista en: http://localhost:${PORT}`);
322
+ logger.info("Presiona Ctrl+C para detener.");
323
+ });
324
+ };
325
+ };
@@ -0,0 +1,72 @@
1
+ import path from 'path';
2
+ import yaml from 'js-yaml';
3
+ import type { TyrContext } from '../Kernel';
4
+
5
+ interface TyrConfig {
6
+ commands: Record<string, string>;
7
+ aliases?: Record<string, string>;
8
+ }
9
+
10
+ const template = `
11
+ import { TyrContext } from '../core/Kernel';
12
+
13
+ export default ({ run, task, fail, logger }: TyrContext) => {
14
+ return async (args: string[]) => {
15
+ logger.info("Ejecutando comando: %s");
16
+
17
+ // Tu lógica aquí...
18
+ // Ejecuta "tyr doc" para ver la documentación
19
+
20
+ logger.success("¡Comando %s finalizado!");
21
+ };
22
+ };
23
+
24
+ export const Test = { args: [ ] };
25
+ `;
26
+
27
+ export default function gen({ logger, fs, frameworkRoot }: TyrContext) {
28
+ return async (args: string[]) => {
29
+ const commandName = args[0];
30
+ const fileName = args[1];
31
+
32
+ if (!commandName || !fileName) {
33
+ logger.error("Uso incorrecto.");
34
+ logger.info("Sintaxis: tyr gen [nombre-comando] [nombre-archivo]");
35
+ return;
36
+ }
37
+
38
+ logger.info(`🔨 Creando nuevo comando: '${commandName}' -> '${fileName}.tyr.ts'`);
39
+
40
+ const filePath = path.resolve(frameworkRoot, 'src/commands', `${fileName}.tyr.ts`);
41
+
42
+ const templateFilled = template.replaceAll('%s', commandName);
43
+
44
+ if (await fs.exists(filePath)) {
45
+ logger.error(`El archivo ${fileName}.tyr.ts ya existe. Abortando.`);
46
+ return;
47
+ }
48
+
49
+ await fs.write(filePath, templateFilled.trim());
50
+
51
+ const configPath = path.resolve(frameworkRoot, 'config/map.yml');
52
+
53
+ try {
54
+ const currentConfigRaw = await fs.read(configPath);
55
+ const config = yaml.load(currentConfigRaw) as unknown as TyrConfig;
56
+
57
+ if (config.commands[commandName]) {
58
+ logger.warn(`El comando '${commandName}' ya existía. Actualizando ruta...`);
59
+ }
60
+
61
+ config.commands[commandName] = `./src/commands/${fileName}.tyr.ts`;
62
+
63
+ const newYaml = yaml.dump(config, { indent: 2, lineWidth: -1 });
64
+ await fs.write(configPath, newYaml);
65
+
66
+ logger.success(`✅ Comando '${commandName}' registrado en el núcleo (.tyr.ts).`);
67
+ } catch (e) {
68
+ logger.error("Error al actualizar la configuración.");
69
+ console.error(e);
70
+ }
71
+ };
72
+ };
@@ -0,0 +1,57 @@
1
+ import path from 'path';
2
+ import yaml from 'js-yaml';
3
+ import type { TyrContext } from '../Kernel';
4
+
5
+ interface TyrConfig {
6
+ commands: Record<string, string>;
7
+ aliases?: Record<string, string>;
8
+ }
9
+
10
+ export default function rem({ logger, fs, frameworkRoot }: TyrContext) {
11
+ return async (args: string[]) => {
12
+ const commandName = args[0];
13
+
14
+ if (!commandName) {
15
+ logger.error("Falta el nombre del comando a eliminar.");
16
+ return;
17
+ }
18
+
19
+ logger.info(`Iniciando eliminación del comando: '${commandName}'`);
20
+ const configPath = path.resolve(frameworkRoot, 'config/map.yml');
21
+
22
+ try {
23
+ const currentConfigRaw = await fs.read(configPath);
24
+ const config = yaml.load(currentConfigRaw) as unknown as TyrConfig;
25
+
26
+ if (!config.commands[commandName]) {
27
+ logger.error(`El comando '${commandName}' no existe.`);
28
+ return;
29
+ }
30
+
31
+ const relativeScriptPath = config.commands[commandName];
32
+ const absoluteScriptPath = path.resolve(frameworkRoot, relativeScriptPath);
33
+
34
+ await fs.delete(absoluteScriptPath);
35
+
36
+ delete config.commands[commandName];
37
+
38
+ if (config.aliases) {
39
+ for (const [alias, target] of Object.entries(config.aliases)) {
40
+ if (target === commandName) {
41
+ delete config.aliases[alias];
42
+ logger.info(`Alias '${alias}' eliminado.`);
43
+ }
44
+ }
45
+ }
46
+
47
+ const newYaml = yaml.dump(config, { indent: 2, lineWidth: -1 });
48
+ await fs.write(configPath, newYaml);
49
+
50
+ logger.success(`Comando '${commandName}' eliminado.`);
51
+
52
+ } catch (e) {
53
+ logger.error("Error crítico durante la eliminación.");
54
+ console.error(e);
55
+ }
56
+ };
57
+ };