@orxataguy/tyr 1.5.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.
- package/LICENSE +21 -21
- package/README.md +408 -408
- package/bin/tyr.ts +13 -13
- package/config/map.yml +4 -7
- package/package.json +60 -62
- package/src/commands/di.tyr.ts +112 -112
- package/src/commands/dw.tyr.ts +115 -115
- package/src/commands/install.tyr.ts +61 -61
- package/src/core/Container.ts +56 -56
- package/src/core/Kernel.ts +1 -1
- package/src/core/Logger.ts +48 -48
- package/src/core/TyrError.ts +57 -57
- package/src/core/sys/ai.ts +162 -162
- package/src/core/sys/config.ts +121 -83
- package/src/core/sys/doc.ts +324 -324
- package/src/lib/DockerManager.ts +108 -108
- package/src/lib/FileSystemManager.ts +152 -152
- package/src/lib/GitManager.ts +75 -75
- package/src/lib/PackageManager.ts +87 -87
- package/src/lib/SQLManager.ts +117 -117
- package/src/lib/ShellManager.ts +117 -117
- package/src/lib/SystemManager.ts +83 -83
- package/src/lib/WebManager.ts +62 -62
package/src/core/sys/ai.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/core/sys/config.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
}
|