@orxataguy/tyr 1.0.0 → 1.0.2

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.
@@ -1,162 +1,160 @@
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
- }
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import axios from 'axios';
4
+ import type { TyrContext } from '../Kernel';
5
+
6
+ type AIProvider = 'claude' | 'openai' | 'gemini';
7
+
8
+ interface AIConfig {
9
+ provider: AIProvider;
10
+ apiKey: string;
11
+ }
12
+
13
+ const PROVIDERS: { env: string; provider: AIProvider }[] = [
14
+ { env: 'CLAUDE_API_KEY', provider: 'claude' },
15
+ { env: 'OPENAI_API_KEY', provider: 'openai' },
16
+ { env: 'GEMINI_API_KEY', provider: 'gemini' },
17
+ ];
18
+
19
+ function detectProvider(): AIConfig | null {
20
+ for (const { env, provider } of PROVIDERS) {
21
+ const key = process.env[env];
22
+ if (key) return { provider, apiKey: key };
23
+ }
24
+ return null;
25
+ }
26
+
27
+ async function callAI(config: AIConfig, sys: string, user: string): Promise<string> {
28
+ switch (config.provider) {
29
+ case 'claude': {
30
+ const res = await axios.post('https://api.anthropic.com/v1/messages', {
31
+ model: 'claude-sonnet-4-20250514',
32
+ max_tokens: 4096,
33
+ system: sys,
34
+ messages: [{ role: 'user', content: user }],
35
+ }, {
36
+ headers: {
37
+ 'x-api-key': config.apiKey,
38
+ 'anthropic-version': '2023-06-01',
39
+ 'content-type': 'application/json',
40
+ },
41
+ });
42
+ return res.data.content[0].text;
43
+ }
44
+ case 'openai': {
45
+ const res = await axios.post('https://api.openai.com/v1/chat/completions', {
46
+ model: 'gpt-4o',
47
+ messages: [
48
+ { role: 'system', content: sys },
49
+ { role: 'user', content: user },
50
+ ],
51
+ max_tokens: 4096,
52
+ }, {
53
+ headers: {
54
+ 'Authorization': `Bearer ${config.apiKey}`,
55
+ 'content-type': 'application/json',
56
+ },
57
+ });
58
+ return res.data.choices[0].message.content;
59
+ }
60
+ case 'gemini': {
61
+ const res = await axios.post(
62
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${config.apiKey}`,
63
+ {
64
+ system_instruction: { parts: [{ text: sys }] },
65
+ contents: [{ parts: [{ text: user }] }],
66
+ },
67
+ { headers: { 'content-type': 'application/json' } }
68
+ );
69
+ return res.data.candidates[0].content.parts[0].text;
70
+ }
71
+ }
72
+ }
73
+
74
+ function extractCodeBlock(response: string): string {
75
+ const match = response.match(/```(?:typescript|ts)?\s*\n([\s\S]*?)```/);
76
+ return match ? match[1].trim() : response.trim();
77
+ }
78
+
79
+ function buildSystemPrompt(frameworkRoot: string): string {
80
+ const libPath = path.resolve(frameworkRoot, 'src/lib');
81
+ let mods = '';
82
+
83
+ if (fs.existsSync(libPath)) {
84
+ for (const file of fs.readdirSync(libPath).filter(f => f.endsWith('.ts'))) {
85
+ const content = fs.readFileSync(path.join(libPath, file), 'utf8');
86
+ const methods: string[] = [];
87
+ const re = /\/\*\*([\s\S]*?)\*\/\s*(public\s+(?:async\s+)?(\w+))?/g;
88
+ let m;
89
+ while ((m = re.exec(content)) !== null) {
90
+ if (m[3]) {
91
+ const desc = m[1].replace(/\*/g, '').replace(/@\w+\s*/g, '').trim().split('\n')[0].trim();
92
+ methods.push(`${m[3]}:${desc}`);
93
+ }
94
+ }
95
+ if (methods.length) mods += `\n${file.replace('.ts', '')}:${methods.join(';')}\n`;
96
+ }
97
+ }
98
+
99
+ return `Genera comando Tyr (TS CLI).
100
+
101
+ FORMATO:
102
+ import{TyrContext}from'../core/Kernel';
103
+ export default({run,task,fail,logger,...mgrs}:TyrContext)=>{
104
+ return async(args:string[])=>{/*impl*/};};
105
+ export const Test={args:['ej1','ej2']};
106
+
107
+ KERNEL:run(cmd,args),task(desc,fn),fail(msg,hint?),logger:{info,success,warn,error}
108
+ MANAGERS(destructurar):shell(exec,cd,input,showLoader),fs(read,write,exists,delete),git(clone),docker,pkg,db,web,sys${mods}
109
+
110
+ REGLAS:export default;async(args:string[]);task() p/errores;fail() p/validar;Test con args realistas.
111
+ Responde SOLO código TS sin explicaciones ni backticks.`;
112
+ }
113
+
114
+ export default function ai({ logger, fs: tyrFs, frameworkRoot, run, fail }: TyrContext) {
115
+ return async (args: string[]) => {
116
+ const commandName = args[0];
117
+ const prompt = args.slice(1).join(' ');
118
+
119
+ if (!commandName || !prompt) {
120
+ return fail(
121
+ "Uso incorrecto de ai.",
122
+ "Sintaxis: tyr ai [nombre-comando] [prompt]"
123
+ );
124
+ }
125
+
126
+ const aiConfig = detectProvider();
127
+ if (!aiConfig) {
128
+ return fail(
129
+ "No se encontró API key de IA.",
130
+ "Configura CLAUDE_API_KEY, OPENAI_API_KEY o GEMINI_API_KEY en .env"
131
+ );
132
+ }
133
+
134
+ logger.success(`API: ${aiConfig.provider}`);
135
+
136
+ logger.info(`Scaffold '${commandName}'...`);
137
+ await run('gen', [commandName, commandName]);
138
+
139
+ const systemPrompt = buildSystemPrompt(frameworkRoot);
140
+
141
+ logger.info(`Enviando a ${aiConfig.provider}...`);
142
+
143
+ let code: string;
144
+ try {
145
+ const response = await callAI(aiConfig, systemPrompt, prompt);
146
+ code = extractCodeBlock(response);
147
+ logger.success(`OK (${code.length} chars)`);
148
+ } catch (e: any) {
149
+ const msg = e.response?.data?.error?.message || e.message;
150
+ return fail(
151
+ `Error ${aiConfig.provider}: ${msg}`,
152
+ `'${commandName}' creado con template base. Revisa tu API key.`
153
+ );
154
+ }
155
+
156
+ const filePath = path.resolve(frameworkRoot, 'src/commands', `${commandName}.tyr.ts`);
157
+ await tyrFs.write(filePath, code);
158
+ logger.success(`'${commandName}' -> src/commands/${commandName}.tyr.ts`);
159
+ };
160
+ }
@@ -0,0 +1,231 @@
1
+ import path from 'path';
2
+ import yaml from 'js-yaml';
3
+ import { homedir, platform } from 'os';
4
+ import { existsSync, cpSync, rmSync } from 'fs';
5
+ import { execSync } from 'child_process';
6
+ import type { TyrContext } from '../Kernel';
7
+
8
+ function removeDirRecursive(dirPath: string): void {
9
+ if (platform() === 'win32') {
10
+ execSync(`rd /s /q "${dirPath}"`, { stdio: 'pipe' });
11
+ } else {
12
+ rmSync(dirPath, { recursive: true, force: true });
13
+ }
14
+ }
15
+
16
+ // ─── Shell RC detection ───────────────────────────────────────────────────────
17
+
18
+ function detectShellRcFile(homeDir: string): string | null {
19
+ const shell = process.env.SHELL || '';
20
+ if (shell.includes('zsh')) return path.join(homeDir, '.zshrc');
21
+ if (shell.includes('fish')) return path.join(homeDir, '.config', 'fish', 'config.fish');
22
+ if (shell.includes('bash')) {
23
+ const candidates = [path.join(homeDir, '.bash_profile'), path.join(homeDir, '.bashrc')];
24
+ return candidates.find(p => existsSync(p)) ?? path.join(homeDir, '.bashrc');
25
+ }
26
+ const fallbacks = [path.join(homeDir, '.zshrc'), path.join(homeDir, '.bashrc'), path.join(homeDir, '.bash_profile')];
27
+ return fallbacks.find(p => existsSync(p)) ?? path.join(homeDir, '.bashrc');
28
+ }
29
+
30
+ // ─── File templates ───────────────────────────────────────────────────────────
31
+
32
+ const ENV_TEMPLATE = `# ~/.tyr/.env
33
+ # Variables de entorno para Tyr. Este archivo nunca debe subirse a git.
34
+ #
35
+ # Base de datos SQL Server
36
+ MSSQL_USER=
37
+ MSSQL_PASSWORD=
38
+ MSSQL_SERVER=
39
+ MSSQL_DATABASE=
40
+ #
41
+ # Proveedores de IA (tyr ai)
42
+ CLAUDE_API_KEY=
43
+ OPENAI_API_KEY=
44
+ GEMINI_API_KEY=
45
+ `;
46
+
47
+ const SH_ALIASES_TEMPLATE = `# ~/.tyr/aliases
48
+ # Añade aquí tus aliases personalizados.
49
+ # Este archivo se carga automáticamente por tu shell.
50
+ #
51
+ # Ejemplos:
52
+ # alias gs='git status'
53
+ # alias tyr-deploy='tyr deploy'
54
+ `;
55
+
56
+ const SH_PLUGINS_TEMPLATE = `# ~/.tyr/plugins
57
+ # Añade aquí tus plugins de shell.
58
+ # Compatible con zsh, bash y otros shells POSIX.
59
+ #
60
+ # Ejemplos (zsh):
61
+ # source /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
62
+ `;
63
+
64
+ const PS_ALIASES_TEMPLATE = `# ~/.tyr/aliases.ps1
65
+ # Añade aquí tus aliases personalizados para PowerShell.
66
+ #
67
+ # Ejemplos:
68
+ # Set-Alias gs git-status
69
+ # function tyr-deploy { tyr deploy @args }
70
+ `;
71
+
72
+ const PS_PLUGINS_TEMPLATE = `# ~/.tyr/plugins.ps1
73
+ # Añade aquí tus módulos y plugins de PowerShell.
74
+ #
75
+ # Ejemplos:
76
+ # Import-Module posh-git
77
+ # Import-Module PSReadLine
78
+ `;
79
+
80
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
81
+
82
+ function makeTimestamp(): string {
83
+ const now = new Date();
84
+ const pad = (n: number) => String(n).padStart(2, '0');
85
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}`;
86
+ }
87
+
88
+ async function configureUnixShell(tyrFs: any, logger: any, homeDir: string, aliasesPath: string, pluginsPath: string): Promise<void> {
89
+ const rcFile = detectShellRcFile(homeDir);
90
+ if (!rcFile) {
91
+ logger.warn('No se pudo detectar el archivo de configuración del shell.');
92
+ logger.info(`Añade manualmente:\n source "${aliasesPath}"\n source "${pluginsPath}"`);
93
+ return;
94
+ }
95
+ await tyrFs.ensureLine(rcFile, `source "${aliasesPath}"`);
96
+ await tyrFs.ensureLine(rcFile, `source "${pluginsPath}"`);
97
+ logger.success(`Shell configurado: ${rcFile}`);
98
+ logger.info(`Ejecuta: source ${rcFile} (o abre una nueva terminal)`);
99
+ }
100
+
101
+ async function configureWindowsShell(tyrFs: any, logger: any, aliasesPath: string, pluginsPath: string): Promise<void> {
102
+ const psProfile = process.env.USERPROFILE
103
+ ? path.join(process.env.USERPROFILE, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1')
104
+ : null;
105
+ if (!psProfile) {
106
+ logger.warn('No se pudo detectar el perfil de PowerShell.');
107
+ logger.info(`Añade manualmente:\n . "${aliasesPath}"\n . "${pluginsPath}"`);
108
+ return;
109
+ }
110
+ await tyrFs.ensureLine(psProfile, `. "${aliasesPath}"`);
111
+ await tyrFs.ensureLine(psProfile, `. "${pluginsPath}"`);
112
+ logger.success(`Perfil de PowerShell configurado: ${psProfile}`);
113
+ logger.info('Reinicia PowerShell para aplicar los cambios.');
114
+ }
115
+
116
+ // ─── Main ─────────────────────────────────────────────────────────────────────
117
+
118
+ export default function config({ logger, fs: tyrFs, frameworkRoot, shell }: TyrContext) {
119
+ return async (args: string[]) => {
120
+ const homeDir = homedir();
121
+ const userRoot = path.join(homeDir, '.tyr');
122
+ const isWindows = platform() === 'win32';
123
+ const ext = isWindows ? '.ps1' : '';
124
+
125
+ // Parse --repo <url>
126
+ const repoIndex = args.indexOf('--repo');
127
+ const repoUrl = repoIndex !== -1 ? (args[repoIndex + 1] ?? null) : null;
128
+
129
+ if (repoIndex !== -1 && (!repoUrl || repoUrl.startsWith('--'))) {
130
+ logger.error('Falta la URL del repositorio.');
131
+ logger.info('Uso: tyr --config --repo <url>');
132
+ return;
133
+ }
134
+
135
+ // ── 1. Backup existing ~/.tyr ──────────────────────────────────────────
136
+ let backupPath: string | null = null;
137
+ if (existsSync(userRoot)) {
138
+ backupPath = `${userRoot}.bak.${makeTimestamp()}`;
139
+ cpSync(userRoot, backupPath, { recursive: true });
140
+ removeDirRecursive(userRoot);
141
+ logger.warn(`Configuración anterior respaldada en: ${backupPath}`);
142
+ }
143
+
144
+ // ── 2. Git clone (if --repo) ───────────────────────────────────────────
145
+ let repoHasContent = false;
146
+ if (repoUrl) {
147
+ logger.info(`\nClonando repositorio: ${repoUrl}`);
148
+ try {
149
+ await shell.exec(`git clone "${repoUrl}" "${userRoot}"`);
150
+ } catch (e) {
151
+ // Restore backup if clone failed
152
+ if (backupPath && existsSync(backupPath)) {
153
+ cpSync(backupPath, userRoot, { recursive: true });
154
+ removeDirRecursive(backupPath);
155
+ logger.warn('Error al clonar. Configuración anterior restaurada.');
156
+ }
157
+ throw e;
158
+ }
159
+ repoHasContent = tyrFs.exists(path.join(userRoot, 'map.yml'));
160
+ logger.success(repoHasContent
161
+ ? 'Repositorio clonado con configuración existente.'
162
+ : 'Repositorio vacío — iniciando configuración por defecto...');
163
+ }
164
+
165
+ // ── 3. Initialize if needed (no repo, or repo was empty) ──────────────
166
+ if (!repoHasContent) {
167
+ logger.info('\nInicializando ~/.tyr...\n');
168
+
169
+ await tyrFs.createDir(path.join(userRoot, 'commands'));
170
+ logger.success(`Directorio creado: ${path.join(userRoot, 'commands')}`);
171
+
172
+ const aliasesPath = path.join(userRoot, `aliases${ext}`);
173
+ if (!tyrFs.exists(aliasesPath)) {
174
+ await tyrFs.write(aliasesPath, isWindows ? PS_ALIASES_TEMPLATE : SH_ALIASES_TEMPLATE);
175
+ logger.success(`Archivo creado: ${aliasesPath}`);
176
+ }
177
+
178
+ const pluginsPath = path.join(userRoot, `plugins${ext}`);
179
+ if (!tyrFs.exists(pluginsPath)) {
180
+ await tyrFs.write(pluginsPath, isWindows ? PS_PLUGINS_TEMPLATE : SH_PLUGINS_TEMPLATE);
181
+ logger.success(`Archivo creado: ${pluginsPath}`);
182
+ }
183
+
184
+ // Write map.yml
185
+ const mapPath = path.join(userRoot, 'map.yml');
186
+ await tyrFs.write(mapPath, 'commands: {}\n');
187
+ logger.success(`Archivo creado: ${mapPath}`);
188
+
189
+ // Write .env template
190
+ const envPath = path.join(userRoot, '.env');
191
+ if (!tyrFs.exists(envPath)) {
192
+ await tyrFs.write(envPath, ENV_TEMPLATE);
193
+ logger.success(`Archivo creado: ${envPath}`);
194
+ }
195
+
196
+ // If linked to a repo, commit and push
197
+ if (repoUrl) {
198
+ logger.info('\nSubiendo configuración inicial al repositorio...');
199
+ shell.cd(userRoot);
200
+ try {
201
+ await shell.exec('git add .');
202
+ await shell.exec('git commit -m "Initial tyr configuration"');
203
+ await shell.exec('git push -u origin HEAD');
204
+ logger.success('Configuración subida al repositorio.');
205
+ } catch (e) {
206
+ logger.warn('No se pudo hacer push automático. Hazlo manualmente desde ~/.tyr');
207
+ }
208
+ }
209
+ }
210
+
211
+ // ── 4. Configure shell (always) ────────────────────────────────────────
212
+ const aliasesPath = path.join(userRoot, `aliases${ext}`);
213
+ const pluginsPath = path.join(userRoot, `plugins${ext}`);
214
+
215
+ if (tyrFs.exists(aliasesPath) || tyrFs.exists(pluginsPath)) {
216
+ logger.info('\nConfigurando shell...');
217
+ if (isWindows) {
218
+ await configureWindowsShell(tyrFs, logger, aliasesPath, pluginsPath);
219
+ } else {
220
+ await configureUnixShell(tyrFs, logger, homeDir, aliasesPath, pluginsPath);
221
+ }
222
+ }
223
+
224
+ logger.success('\nTyr configurado correctamente.');
225
+ logger.info(`Directorio de configuración: ${userRoot}`);
226
+ if (repoUrl) logger.info(`Repositorio vinculado: ${repoUrl}`);
227
+ logger.info('\nPróximos pasos:');
228
+ logger.info(' tyr gen <nombre> <archivo> Crear un nuevo comando');
229
+ logger.info(' tyr doc Ver documentación de la API');
230
+ };
231
+ }