@orxataguy/tyr 1.4.0 → 1.6.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,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
- }
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
+ }
@@ -2,26 +2,25 @@ import path from 'path';
2
2
  import yaml from 'js-yaml';
3
3
  import { homedir, platform } from 'os';
4
4
  import { existsSync } from 'fs';
5
+ import { rename } from 'fs/promises';
5
6
  import type { TyrContext } from '../Kernel';
6
7
 
8
+ // ─── Shell RC detection ───────────────────────────────────────────────────────
9
+
7
10
  function detectShellRcFile(homeDir: string): string | null {
8
11
  const shell = process.env.SHELL || '';
9
-
10
12
  if (shell.includes('zsh')) return path.join(homeDir, '.zshrc');
11
13
  if (shell.includes('fish')) return path.join(homeDir, '.config', 'fish', 'config.fish');
12
14
  if (shell.includes('bash')) {
13
15
  const candidates = [path.join(homeDir, '.bash_profile'), path.join(homeDir, '.bashrc')];
14
16
  return candidates.find(p => existsSync(p)) ?? path.join(homeDir, '.bashrc');
15
17
  }
16
-
17
- const fallbacks = [
18
- path.join(homeDir, '.zshrc'),
19
- path.join(homeDir, '.bashrc'),
20
- path.join(homeDir, '.bash_profile'),
21
- ];
18
+ const fallbacks = [path.join(homeDir, '.zshrc'), path.join(homeDir, '.bashrc'), path.join(homeDir, '.bash_profile')];
22
19
  return fallbacks.find(p => existsSync(p)) ?? path.join(homeDir, '.bashrc');
23
20
  }
24
21
 
22
+ // ─── File templates ───────────────────────────────────────────────────────────
23
+
25
24
  const SH_ALIASES_TEMPLATE = `# ~/.tyr/aliases
26
25
  # Añade aquí tus aliases personalizados.
27
26
  # Este archivo se carga automáticamente por tu shell.
@@ -55,80 +54,15 @@ const PS_PLUGINS_TEMPLATE = `# ~/.tyr/plugins.ps1
55
54
  # Import-Module PSReadLine
56
55
  `;
57
56
 
58
- export default function config({ logger, fs: tyrFs, frameworkRoot }: TyrContext) {
59
- return async (_args: string[]) => {
60
- const homeDir = homedir();
61
- const userRoot = path.join(homeDir, '.tyr');
62
- const isWindows = platform() === 'win32';
63
- const ext = isWindows ? '.ps1' : '';
64
-
65
- logger.info('Iniciando configuración de Tyr...\n');
66
-
67
- // 1. ~/.tyr/commands/
68
- await tyrFs.createDir(path.join(userRoot, 'commands'));
69
- logger.success(`Directorio creado: ${path.join(userRoot, 'commands')}`);
70
-
71
- // 2. ~/.tyr/aliases(.ps1)
72
- const aliasesPath = path.join(userRoot, `aliases${ext}`);
73
- if (!tyrFs.exists(aliasesPath)) {
74
- await tyrFs.write(aliasesPath, isWindows ? PS_ALIASES_TEMPLATE : SH_ALIASES_TEMPLATE);
75
- logger.success(`Archivo creado: ${aliasesPath}`);
76
- } else {
77
- logger.info(`Ya existe: ${aliasesPath}`);
78
- }
79
-
80
- // 3. ~/.tyr/plugins(.ps1)
81
- const pluginsPath = path.join(userRoot, `plugins${ext}`);
82
- if (!tyrFs.exists(pluginsPath)) {
83
- await tyrFs.write(pluginsPath, isWindows ? PS_PLUGINS_TEMPLATE : SH_PLUGINS_TEMPLATE);
84
- logger.success(`Archivo creado: ${pluginsPath}`);
85
- } else {
86
- logger.info(`Ya existe: ${pluginsPath}`);
87
- }
88
-
89
- // 4. ~/.tyr/map.yml — create or update, registering framework commands with absolute paths
90
- const mapPath = path.join(userRoot, 'map.yml');
91
- const currentRaw = await tyrFs.read(mapPath);
92
- const userConfig: { commands: Record<string, string> } =
93
- (yaml.load(currentRaw ?? '') as any) ?? { commands: {} };
94
- if (!userConfig.commands) userConfig.commands = {};
95
-
96
- const frameworkMapPath = path.join(frameworkRoot, 'config', 'map.yml');
97
- if (existsSync(frameworkMapPath)) {
98
- const frameworkRaw = await tyrFs.read(frameworkMapPath);
99
- const frameworkConfig = (yaml.load(frameworkRaw ?? '') as any) ?? {};
100
- for (const [name, relPath] of Object.entries(frameworkConfig.commands ?? {})) {
101
- const absPath = path.resolve(frameworkRoot, relPath as string);
102
- if (existsSync(absPath) && !userConfig.commands[name]) {
103
- userConfig.commands[name] = absPath;
104
- logger.info(` Comando registrado: ${name}`);
105
- }
106
- }
107
- }
57
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
108
58
 
109
- await tyrFs.write(mapPath, yaml.dump(userConfig, { indent: 2, lineWidth: -1 }));
110
- logger.success(`Configuración guardada: ${mapPath}`);
111
-
112
- // 5. Configure shell to source aliases and plugins
113
- logger.info('\nConfigurando shell...');
114
- if (isWindows) {
115
- await configureWindowsShell(tyrFs, logger, aliasesPath, pluginsPath);
116
- } else {
117
- await configureUnixShell(tyrFs, logger, homeDir, aliasesPath, pluginsPath);
118
- }
119
-
120
- logger.success('\nTyr configurado correctamente.');
121
- logger.info(`\nDirectorio de configuración: ${userRoot}`);
122
- logger.info('\nPróximos pasos:');
123
- logger.info(' tyr gen <nombre> <archivo> Crear un nuevo comando');
124
- logger.info(' tyr doc Ver documentación de la API');
125
- };
59
+ function makeTimestamp(): string {
60
+ const now = new Date();
61
+ const pad = (n: number) => String(n).padStart(2, '0');
62
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}`;
126
63
  }
127
64
 
128
- async function configureUnixShell(
129
- tyrFs: any, logger: any,
130
- homeDir: string, aliasesPath: string, pluginsPath: string
131
- ): Promise<void> {
65
+ async function configureUnixShell(tyrFs: any, logger: any, homeDir: string, aliasesPath: string, pluginsPath: string): Promise<void> {
132
66
  const rcFile = detectShellRcFile(homeDir);
133
67
  if (!rcFile) {
134
68
  logger.warn('No se pudo detectar el archivo de configuración del shell.');
@@ -141,14 +75,10 @@ async function configureUnixShell(
141
75
  logger.info(`Ejecuta: source ${rcFile} (o abre una nueva terminal)`);
142
76
  }
143
77
 
144
- async function configureWindowsShell(
145
- tyrFs: any, logger: any,
146
- aliasesPath: string, pluginsPath: string
147
- ): Promise<void> {
78
+ async function configureWindowsShell(tyrFs: any, logger: any, aliasesPath: string, pluginsPath: string): Promise<void> {
148
79
  const psProfile = process.env.USERPROFILE
149
80
  ? path.join(process.env.USERPROFILE, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1')
150
81
  : null;
151
-
152
82
  if (!psProfile) {
153
83
  logger.warn('No se pudo detectar el perfil de PowerShell.');
154
84
  logger.info(`Añade manualmente:\n . "${aliasesPath}"\n . "${pluginsPath}"`);
@@ -159,3 +89,111 @@ async function configureWindowsShell(
159
89
  logger.success(`Perfil de PowerShell configurado: ${psProfile}`);
160
90
  logger.info('Reinicia PowerShell para aplicar los cambios.');
161
91
  }
92
+
93
+ // ─── Main ─────────────────────────────────────────────────────────────────────
94
+
95
+ export default function config({ logger, fs: tyrFs, frameworkRoot, shell }: TyrContext) {
96
+ return async (args: string[]) => {
97
+ const homeDir = homedir();
98
+ const userRoot = path.join(homeDir, '.tyr');
99
+ const isWindows = platform() === 'win32';
100
+ const ext = isWindows ? '.ps1' : '';
101
+
102
+ // Parse --repo <url>
103
+ const repoIndex = args.indexOf('--repo');
104
+ const repoUrl = repoIndex !== -1 ? (args[repoIndex + 1] ?? null) : null;
105
+
106
+ if (repoIndex !== -1 && (!repoUrl || repoUrl.startsWith('--'))) {
107
+ logger.error('Falta la URL del repositorio.');
108
+ logger.info('Uso: tyr --config --repo <url>');
109
+ return;
110
+ }
111
+
112
+ // ── 1. Backup existing ~/.tyr ──────────────────────────────────────────
113
+ let backupPath: string | null = null;
114
+ if (existsSync(userRoot)) {
115
+ backupPath = `${userRoot}.bak.${makeTimestamp()}`;
116
+ await rename(userRoot, backupPath);
117
+ logger.warn(`Configuración anterior respaldada en: ${backupPath}`);
118
+ }
119
+
120
+ // ── 2. Git clone (if --repo) ───────────────────────────────────────────
121
+ let repoHasContent = false;
122
+ if (repoUrl) {
123
+ logger.info(`\nClonando repositorio: ${repoUrl}`);
124
+ try {
125
+ await shell.exec(`git clone "${repoUrl}" "${userRoot}"`);
126
+ } catch (e) {
127
+ // Restore backup if clone failed
128
+ if (backupPath && existsSync(backupPath)) {
129
+ await rename(backupPath, userRoot);
130
+ logger.warn('Error al clonar. Configuración anterior restaurada.');
131
+ }
132
+ throw e;
133
+ }
134
+ repoHasContent = tyrFs.exists(path.join(userRoot, 'map.yml'));
135
+ logger.success(repoHasContent
136
+ ? 'Repositorio clonado con configuración existente.'
137
+ : 'Repositorio vacío — iniciando configuración por defecto...');
138
+ }
139
+
140
+ // ── 3. Initialize if needed (no repo, or repo was empty) ──────────────
141
+ if (!repoHasContent) {
142
+ logger.info('\nInicializando ~/.tyr...\n');
143
+
144
+ await tyrFs.createDir(path.join(userRoot, 'commands'));
145
+ logger.success(`Directorio creado: ${path.join(userRoot, 'commands')}`);
146
+
147
+ const aliasesPath = path.join(userRoot, `aliases${ext}`);
148
+ if (!tyrFs.exists(aliasesPath)) {
149
+ await tyrFs.write(aliasesPath, isWindows ? PS_ALIASES_TEMPLATE : SH_ALIASES_TEMPLATE);
150
+ logger.success(`Archivo creado: ${aliasesPath}`);
151
+ }
152
+
153
+ const pluginsPath = path.join(userRoot, `plugins${ext}`);
154
+ if (!tyrFs.exists(pluginsPath)) {
155
+ await tyrFs.write(pluginsPath, isWindows ? PS_PLUGINS_TEMPLATE : SH_PLUGINS_TEMPLATE);
156
+ logger.success(`Archivo creado: ${pluginsPath}`);
157
+ }
158
+
159
+ // Write map.yml
160
+ const mapPath = path.join(userRoot, 'map.yml');
161
+ await tyrFs.write(mapPath, 'commands: {}\n');
162
+ logger.success(`Archivo creado: ${mapPath}`);
163
+
164
+ // If linked to a repo, commit and push
165
+ if (repoUrl) {
166
+ logger.info('\nSubiendo configuración inicial al repositorio...');
167
+ shell.cd(userRoot);
168
+ try {
169
+ await shell.exec('git add .');
170
+ await shell.exec('git commit -m "Initial tyr configuration"');
171
+ await shell.exec('git push -u origin HEAD');
172
+ logger.success('Configuración subida al repositorio.');
173
+ } catch (e) {
174
+ logger.warn('No se pudo hacer push automático. Hazlo manualmente desde ~/.tyr');
175
+ }
176
+ }
177
+ }
178
+
179
+ // ── 4. Configure shell (always) ────────────────────────────────────────
180
+ const aliasesPath = path.join(userRoot, `aliases${ext}`);
181
+ const pluginsPath = path.join(userRoot, `plugins${ext}`);
182
+
183
+ if (tyrFs.exists(aliasesPath) || tyrFs.exists(pluginsPath)) {
184
+ logger.info('\nConfigurando shell...');
185
+ if (isWindows) {
186
+ await configureWindowsShell(tyrFs, logger, aliasesPath, pluginsPath);
187
+ } else {
188
+ await configureUnixShell(tyrFs, logger, homeDir, aliasesPath, pluginsPath);
189
+ }
190
+ }
191
+
192
+ logger.success('\nTyr configurado correctamente.');
193
+ logger.info(`Directorio de configuración: ${userRoot}`);
194
+ if (repoUrl) logger.info(`Repositorio vinculado: ${repoUrl}`);
195
+ logger.info('\nPróximos pasos:');
196
+ logger.info(' tyr gen <nombre> <archivo> Crear un nuevo comando');
197
+ logger.info(' tyr doc Ver documentación de la API');
198
+ };
199
+ }