@qubiit/lmagent 3.1.9 → 3.3.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.
package/install.js CHANGED
@@ -1,1520 +1,1573 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
- const { Command } = require('commander');
7
- const chalk = require('chalk');
8
- const inquirer = require('inquirer');
9
- const figlet = require('figlet');
10
- const gradient = require('gradient-string');
11
-
12
- const program = new Command();
13
- const PKG_VERSION = require('./package.json').version;
14
-
15
- // Configuración: Directorios fuente del paquete
16
- const PACKAGE_SKILLS_DIR = path.join(__dirname, '.agents', 'skills');
17
- const PACKAGE_RULES_DIR = path.join(__dirname, '.agents', 'rules');
18
- const PACKAGE_WORKFLOWS_DIR = path.join(__dirname, '.agents', 'workflows');
19
- const PACKAGE_CONFIG_DIR = path.join(__dirname, '.agents', 'config');
20
- const PACKAGE_TEMPLATES_DIR = path.join(__dirname, '.agents', 'templates');
21
- const PACKAGE_DOCS_DIR = path.join(__dirname, '.agents', 'docs');
22
- const PACKAGE_MEMORY_DIR = path.join(__dirname, '.agents', 'memory');
23
-
24
- // Archivos de proyecto que init copia a la raíz
25
- // Usan {{VERSION}} como placeholder; se reemplaza dinámicamente al instalar
26
- const INIT_FILES = [
27
- { src: 'CLAUDE.md', desc: 'Instrucciones para Claude Code / Antigravity', versionTemplate: true },
28
- { src: 'GEMINI.md', desc: 'Instrucciones para Gemini CLI / Antigravity', versionTemplate: true },
29
- { src: 'AGENTS.md', desc: 'Catálogo de capacidades LMAgent', versionTemplate: false },
30
- ];
31
-
32
- const INIT_DIRS = [
33
- { src: 'config', desc: 'Configuración del framework' },
34
- { src: 'templates', desc: 'Templates de proyecto' },
35
- { src: 'docs', desc: 'Documentación extendida' },
36
- { src: 'workflows', desc: 'SOPs y Procedimientos' },
37
- ];
38
-
39
- // IDE_CONFIGS: Lista ÚNICA y DEDUPLICADA de todos los agentes soportados
40
- const IDE_CONFIGS = [
41
- // --- IDEs Principales (Auto-Detectados) ---
42
- // Cursor: usa .cursor/rules/*.mdc (formato MDC con frontmatter)
43
- { name: 'Cursor', value: 'cursor', rulesDir: '.cursor/rules', skillsDir: '.cursor/rules/skills', workflowsDir: '.cursor/workflows', configFile: '.cursorrules', bridgeFile: 'lmagent.mdc', markerFile: '.cursorrules', forceCopy: true },
44
- // Windsurf Wave 8+: usa .windsurf/rules/*.md (directorio, NO .windsurfrules)
45
- { name: 'Windsurf', value: 'windsurf', rulesDir: '.windsurf/rules', skillsDir: '.windsurf/skills', workflowsDir: '.windsurf/workflows', configFile: null, bridgeFile: 'lmagent.md', markerFile: '.windsurf', forceCopy: true },
46
- // Cline: usa .clinerules/ (directorio con .md files)
47
- { name: 'Cline', value: 'cline', rulesDir: '.clinerules', skillsDir: '.cline/skills', workflowsDir: '.cline/workflows', configFile: null, bridgeFile: '00-lmagent.md', markerFile: '.clinerules', forceCopy: true },
48
- // Roo Code: usa .roo/rules/ (NO .clinerules, que es de Cline)
49
- { name: 'Roo Code', value: 'roo', rulesDir: '.roo/rules', skillsDir: '.roo/skills', workflowsDir: '.roo/workflows', configFile: null, bridgeFile: '00-lmagent.md', markerFile: '.roo', forceCopy: true },
50
- // GitHub Copilot: usa .github/copilot-instructions.md + .github/instructions/*.md
51
- { name: 'VSCode Copilot', value: 'vscode', rulesDir: '.github/instructions', skillsDir: '.github/skills', workflowsDir: '.github/workflows', configFile: '.github/copilot-instructions.md', bridgeFile: null, markerFile: '.vscode' },
52
- { name: 'Trae', value: 'trae', rulesDir: '.trae/rules', skillsDir: '.trae/skills', workflowsDir: '.trae/workflows', configFile: null, bridgeFile: 'lmagent.md', markerFile: '.trae', forceCopy: true },
53
- // Claude Code: usa CLAUDE.md (raíz) + .claude/rules/ + .claude/skills/
54
- { name: 'Claude Code', value: 'claude', rulesDir: '.claude/rules', skillsDir: '.claude/skills', workflowsDir: '.claude/workflows', configFile: 'CLAUDE.md', bridgeFile: null, markerFile: '.claude', forceCopy: true },
55
- { name: 'Zed', value: 'zed', rulesDir: '.rules', skillsDir: '.rules/skills', workflowsDir: '.rules/workflows', configFile: null, bridgeFile: 'lmagent.md', markerFile: '.zed' },
56
-
57
- // --- Agentes CLI & Autónomos ---
58
- { name: 'Amp / Kimi / Replit', value: 'amp', rulesDir: '.agents/rules', skillsDir: '.agents/skills', workflowsDir: '.agents/workflows', configFile: null, bridgeFile: null, markerFile: '.agents' },
59
- // Antigravity (Google Deepmind): usa GEMINI.md (raíz) + .agent/skills/ + .agent/rules/
60
- { name: 'Antigravity', value: 'antigravity', rulesDir: '.agent/rules', skillsDir: '.agent/skills', workflowsDir: '.agent/workflows', configFile: 'GEMINI.md', bridgeFile: null, markerFile: '.agent' },
61
- { name: 'Augment', value: 'augment', rulesDir: '.augment/rules', skillsDir: '.augment/skills', workflowsDir: '.augment/workflows', configFile: null, bridgeFile: null, markerFile: '.augment' },
62
- // Gemini CLI: usa GEMINI.md (raíz) + .gemini/skills/ (oficial) o .agents/skills/ (fallback)
63
- { name: 'Gemini CLI', value: 'gemini', rulesDir: '.gemini/rules', skillsDir: '.gemini/skills', workflowsDir: '.gemini/workflows', configFile: 'GEMINI.md', bridgeFile: null, markerFile: '.gemini' },
64
- { name: 'OpenClaw / Envoid', value: 'openclaw', rulesDir: 'rules', skillsDir: 'skills', workflowsDir: 'workflows', configFile: 'openclaw.json', configTemplate: 'openclaw.json', bridgeFile: null, markerFile: 'openclaw.json' },
65
- { name: 'CodeBuddy', value: 'codebuddy', rulesDir: '.codebuddy/rules', skillsDir: '.codebuddy/skills', workflowsDir: '.codebuddy/workflows', configFile: null, bridgeFile: null, markerFile: '.codebuddy', forceCopy: true },
66
- // Codex CLI (OpenAI): usa AGENTS.md en raíz + .codex/ como directorio de config
67
- { name: 'Codex', value: 'codex', rulesDir: '.codex', skillsDir: '.codex/skills', workflowsDir: '.codex/workflows', configFile: 'AGENTS.md', bridgeFile: null, markerFile: '.codex' },
68
- { name: 'Command Code', value: 'command-code', rulesDir: '.commandcode/rules', skillsDir: '.commandcode/skills', workflowsDir: '.commandcode/workflows', configFile: null, bridgeFile: null, markerFile: '.commandcode' },
69
- // Continue: soporta .continuerules (raíz) + .continue/rules/ (directorio)
70
- { name: 'Continue', value: 'continue', rulesDir: '.continue/rules', skillsDir: '.continue/skills', workflowsDir: '.continue/workflows', configFile: '.continuerules', configTemplate: 'continuerules.md', bridgeFile: '00-lmagent.md', markerFile: '.continue' },
71
- { name: 'Crush', value: 'crush', rulesDir: '.crush/rules', skillsDir: '.crush/skills', workflowsDir: '.crush/workflows', configFile: null, bridgeFile: null, markerFile: '.crush' },
72
- { name: 'Droid', value: 'droid', rulesDir: '.factory/rules', skillsDir: '.factory/skills', workflowsDir: '.factory/workflows', configFile: null, bridgeFile: null, markerFile: '.factory' },
73
- // Goose (Block): usa .goosehints en raíz para instrucciones al agente
74
- { name: 'Goose', value: 'goose', rulesDir: '.goose', skillsDir: '.goose/skills', workflowsDir: '.goose/workflows', configFile: '.goosehints', configTemplate: 'goosehints.md', bridgeFile: null, markerFile: '.goose' },
75
- // Junie (JetBrains): usa .junie/guidelines.md como archivo de instrucciones
76
- { name: 'Junie', value: 'junie', rulesDir: '.junie', skillsDir: '.junie/skills', workflowsDir: '.junie/workflows', configFile: '.junie/guidelines.md', configTemplate: 'junie-guidelines.md', bridgeFile: null, markerFile: '.junie' },
77
- { name: 'iFlow CLI', value: 'iflow', rulesDir: '.iflow/rules', skillsDir: '.iflow/skills', workflowsDir: '.iflow/workflows', configFile: null, bridgeFile: null, markerFile: '.iflow' },
78
- { name: 'Kilo Code', value: 'kilo', rulesDir: '.kilocode/rules', skillsDir: '.kilocode/skills', workflowsDir: '.kilocode/workflows', configFile: null, bridgeFile: null, markerFile: '.kilocode' },
79
- { name: 'Kiro CLI', value: 'kiro', rulesDir: '.kiro/rules', skillsDir: '.kiro/skills', workflowsDir: '.kiro/workflows', configFile: null, bridgeFile: null, markerFile: '.kiro' },
80
- { name: 'Kode', value: 'kode', rulesDir: '.kode/rules', skillsDir: '.kode/skills', workflowsDir: '.kode/workflows', configFile: null, bridgeFile: null, markerFile: '.kode' },
81
- { name: 'MCPJam', value: 'mcpjam', rulesDir: '.mcpjam/rules', skillsDir: '.mcpjam/skills', workflowsDir: '.mcpjam/workflows', configFile: null, bridgeFile: null, markerFile: '.mcpjam' },
82
- { name: 'Mistral Vibe', value: 'mistral', rulesDir: '.vibe/rules', skillsDir: '.vibe/skills', workflowsDir: '.vibe/workflows', configFile: null, bridgeFile: null, markerFile: '.vibe' },
83
- { name: 'Mux', value: 'mux', rulesDir: '.mux/rules', skillsDir: '.mux/skills', workflowsDir: '.mux/workflows', configFile: null, bridgeFile: null, markerFile: '.mux' },
84
- { name: 'OpenCode', value: 'opencode', rulesDir: '.opencode/rules', skillsDir: '.opencode/skills', workflowsDir: '.opencode/workflows', configFile: null, bridgeFile: null, markerFile: '.opencode' },
85
- // OpenHands: usa .openhands/microagents/repo.md para instrucciones del repo
86
- { name: 'OpenHands', value: 'openhands', rulesDir: '.openhands/microagents', skillsDir: '.openhands/skills', workflowsDir: '.openhands/workflows', configFile: '.openhands/microagents/repo.md', configTemplate: 'openhands-repo.md', bridgeFile: null, markerFile: '.openhands' },
87
- { name: 'Pi', value: 'pi', rulesDir: '.pi/rules', skillsDir: '.pi/skills', workflowsDir: '.pi/workflows', configFile: null, bridgeFile: null, markerFile: '.pi' },
88
- { name: 'Qoder', value: 'qoder', rulesDir: '.qoder/rules', skillsDir: '.qoder/skills', workflowsDir: '.qoder/workflows', configFile: null, bridgeFile: null, markerFile: '.qoder' },
89
- { name: 'Qwen Code', value: 'qwen', rulesDir: '.qwen/rules', skillsDir: '.qwen/skills', workflowsDir: '.qwen/workflows', configFile: null, bridgeFile: null, markerFile: '.qwen' },
90
- { name: 'Trae CN', value: 'trae-cn', rulesDir: '.trae-cn/rules', skillsDir: '.trae-cn/skills', workflowsDir: '.trae-cn/workflows', configFile: null, bridgeFile: 'lmagent.md', markerFile: '.trae-cn' },
91
- { name: 'Zencoder', value: 'zencoder', rulesDir: '.zencoder/rules', skillsDir: '.zencoder/skills', workflowsDir: '.zencoder/workflows', configFile: null, bridgeFile: null, markerFile: '.zencoder' },
92
- { name: 'Neovate', value: 'neovate', rulesDir: '.neovate/rules', skillsDir: '.neovate/skills', workflowsDir: '.neovate/workflows', configFile: null, bridgeFile: null, markerFile: '.neovate' },
93
- { name: 'Pochi', value: 'pochi', rulesDir: '.pochi/rules', skillsDir: '.pochi/skills', workflowsDir: '.pochi/workflows', configFile: null, bridgeFile: null, markerFile: '.pochi' },
94
- { name: 'AdaL', value: 'adal', rulesDir: '.adal/rules', skillsDir: '.adal/skills', workflowsDir: '.adal/workflows', configFile: null, bridgeFile: null, markerFile: '.adal' },
95
-
96
- // --- Opciones Especiales ---
97
- { name: 'Generic/Other', value: 'generic', rulesDir: '.agents/rules', skillsDir: '.agents/skills', workflowsDir: '.agents/workflows', configFile: 'AGENTS.md', bridgeFile: null, markerFile: '.agents' },
98
- { name: 'Custom Path (Manual)', value: 'custom', rulesDir: '', skillsDir: '', workflowsDir: '', configFile: null, bridgeFile: null, markerFile: '' },
99
- ];
100
-
101
- program
102
- .name('lmagent')
103
- .description('CLI para instalar skills y reglas de LMAgent')
104
- .version(PKG_VERSION);
105
-
106
- program.command('install')
107
- .description('Instalar skills, rules y workflows en el IDE del proyecto')
108
- .option('-f, --force', 'Forzar instalación')
109
- .option('-y, --yes', 'Instalar todo sin preguntar')
110
- .option('-g, --global', 'También sincronizar al repositorio global (~/.agents/)')
111
- .action((options) => {
112
- runInstall(options);
113
- });
114
-
115
- program.command('update')
116
- .description('Actualizar skills y reglas en el proyecto (alias de install)')
117
- .option('-f, --force', 'Forzar actualización')
118
- .option('-y, --yes', 'Instalar todo sin preguntar')
119
- .option('-g, --global', 'También sincronizar al repositorio global (~/.agents/)')
120
- .action((options) => {
121
- console.log(chalk.blue('ℹ Iniciando actualización...'));
122
- runInstall(options);
123
- });
124
-
125
- program.command('init')
126
- .description('Inicializar proyecto con LMAgent (copia CLAUDE.md, AGENTS.md, config, etc.)')
127
- .option('-f, --force', 'Sobrescribir archivos existentes')
128
- .option('-y, --yes', 'No preguntar, instalar todo')
129
- .action((options) => {
130
- runInit(options);
131
- });
132
-
133
- program.command('doctor')
134
- .description('Verificar que el proyecto está correctamente configurado')
135
- .action(() => {
136
- runDoctor();
137
- });
138
-
139
- program.command('validate')
140
- .description('Validar integridad de todos los skills (frontmatter, estructura)')
141
- .argument('[skill]', 'Nombre parcial del skill a validar (opcional)')
142
- .action((skill) => {
143
- const { execSync } = require('child_process');
144
- const scriptPath = path.join(__dirname, 'scripts', 'validate_skills.js');
145
- const args = skill ? ` ${skill}` : '';
146
- try {
147
- execSync(`node "${scriptPath}"${args}`, { stdio: 'inherit' });
148
- } catch (e) {
149
- process.exit(e.status || 1);
150
- }
151
- });
152
-
153
- program.command('create-skill')
154
- .description('Crear un nuevo skill interactivamente')
155
- .action(() => {
156
- const { execSync } = require('child_process');
157
- const scriptPath = path.join(__dirname, 'scripts', 'create_skill.js');
158
- try {
159
- execSync(`node "${scriptPath}"`, { stdio: 'inherit' });
160
- } catch (e) {
161
- process.exit(e.status || 1);
162
- }
163
- });
164
-
165
- program.command('tokens')
166
- .description('Analizar consumo de tokens del framework instalado en el proyecto')
167
- .option('--json', 'Salida en formato JSON')
168
- .option('--report', 'Generar reporte en .agents/token-report.md')
169
- .action((options) => {
170
- const { execSync } = require('child_process');
171
- const scriptPath = path.join(__dirname, 'scripts', 'token-analyzer.js');
172
- const args = [options.json ? '--json' : '', options.report ? '--report' : ''].filter(Boolean).join(' ');
173
- try {
174
- execSync(`node "${scriptPath}" ${args}`, { stdio: 'inherit' });
175
- } catch (e) {
176
- process.exit(e.status || 1);
177
- }
178
- });
179
-
180
- program.command('skills')
181
- .description('Gestionar skills externos desde GitHub (compatible con el estándar skills.sh)')
182
- .argument('<action>', 'Acción: add')
183
- .argument('<source>', 'Repositorio GitHub: owner/repo o URL completa')
184
- .option('--skill <name>', 'Nombre específico del skill a instalar (opcional)')
185
- .action(async (action, source, opts) => {
186
- if (action !== 'add') {
187
- console.error(chalk.red(`❌ Acción desconocida: ${action}. Usa: lmagent skills add <owner/repo>`));
188
- process.exit(1);
189
- }
190
- const { execSync } = require('child_process');
191
- const repoSlug = source.replace('https://github.com/', '').replace(/\.git$/, '');
192
- const [owner, repo] = repoSlug.split('/');
193
- if (!owner || !repo) {
194
- console.error(chalk.red('❌ Formato inválido. Usa: lmagent skills add owner/repo'));
195
- process.exit(1);
196
- }
197
- const tmpDir = path.join(os.tmpdir(), `lmagent-skill-${Date.now()}`);
198
- const targetSkillsDir = path.join(process.cwd(), '.agents', 'skills');
199
- console.log(chalk.cyan(`📦 Descargando skill desde github.com/${owner}/${repo}...`));
200
- try {
201
- execSync(`git clone --depth 1 https://github.com/${owner}/${repo} "${tmpDir}"`, { stdio: 'pipe' });
202
- const skillsPath = fs.existsSync(path.join(tmpDir, 'skills')) ? path.join(tmpDir, 'skills') : tmpDir;
203
- const items = fs.readdirSync(skillsPath).filter(i => {
204
- const p = path.join(skillsPath, i);
205
- return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'SKILL.md'));
206
- });
207
- if (items.length === 0) {
208
- console.log(chalk.yellow('⚠️ No se encontraron skills con SKILL.md en el repositorio.'));
209
- fs.rmSync(tmpDir, { recursive: true, force: true });
210
- return;
211
- }
212
- const toInstall = opts.skill ? items.filter(i => i.includes(opts.skill)) : items;
213
- if (!fs.existsSync(targetSkillsDir)) fs.mkdirSync(targetSkillsDir, { recursive: true });
214
- for (const skill of toInstall) {
215
- const src = path.join(skillsPath, skill);
216
- const dest = path.join(targetSkillsDir, skill);
217
- copyRecursiveSync(src, dest, true);
218
- console.log(` ${chalk.green('✔')} ${skill}/`);
219
- }
220
- fs.rmSync(tmpDir, { recursive: true, force: true });
221
- console.log(chalk.green(`✨ ${toInstall.length} skill(s) instalado(s) en .agents/skills/`));
222
- console.log(chalk.dim(' Ejecuta `lmagent install` para sincronizarlos a tu agente.'));
223
- } catch (e) {
224
- console.error(chalk.red(`❌ Error al instalar skill: ${e.message}`));
225
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { }
226
- process.exit(1);
227
- }
228
- });
229
-
230
- program.command('uninstall')
231
- .description('Eliminar todos los archivos instalados por LMAgent del proyecto')
232
- .option('-f, --force', 'No pedir confirmación, eliminar directamente')
233
- .option('--all', 'También eliminar entry points raíz (CLAUDE.md, GEMINI.md, AGENTS.md)')
234
- .action(async (options) => {
235
- console.clear();
236
- const branding = figlet.textSync('LMAGENT', { font: 'ANSI Shadow' });
237
- console.log(gradient.pastel.multiline(branding));
238
- console.log(gradient.cristal(' Uninstall - Limpieza\n'));
239
-
240
- const projectRoot = process.cwd();
241
-
242
- // Detectar qué agentes están instalados en el proyecto
243
- const installedIdes = IDE_CONFIGS.filter(ide => {
244
- if (ide.value === 'custom' || ide.value === 'generic') return false;
245
- const markerInProject = ide.markerFile && fs.existsSync(path.join(projectRoot, ide.markerFile));
246
- const rulesDirInProject = ide.rulesDir && fs.existsSync(path.join(projectRoot, ide.rulesDir));
247
- const skillsDirInProject = ide.skillsDir && fs.existsSync(path.join(projectRoot, ide.skillsDir));
248
- return markerInProject || rulesDirInProject || skillsDirInProject;
249
- });
250
-
251
- if (installedIdes.length === 0) {
252
- console.log(chalk.yellow('⚠️ No se detectó ningún agente instalado en este proyecto.'));
253
- return;
254
- }
255
-
256
- console.log(chalk.bold('🔍 Agentes detectados en este proyecto:\n'));
257
- for (const ide of installedIdes) {
258
- const parts = [];
259
- if (ide.rulesDir && fs.existsSync(path.join(projectRoot, ide.rulesDir))) parts.push(chalk.gray(ide.rulesDir));
260
- if (ide.skillsDir && fs.existsSync(path.join(projectRoot, ide.skillsDir))) parts.push(chalk.gray(ide.skillsDir));
261
- if (ide.configFile && fs.existsSync(path.join(projectRoot, ide.configFile))) parts.push(chalk.gray(ide.configFile));
262
- console.log(` ${chalk.cyan('•')} ${chalk.bold(ide.name)}: ${parts.join(', ')}`);
263
- }
264
-
265
- // Entry points raíz
266
- const rootFiles = ['CLAUDE.md', 'GEMINI.md', 'AGENTS.md', '.cursorrules', '.windsurfrules', '.continuerules', '.goosehints'];
267
- const existingRootFiles = rootFiles.filter(f => fs.existsSync(path.join(projectRoot, f)));
268
-
269
- if (options.all && existingRootFiles.length > 0) {
270
- console.log(chalk.bold('\n📄 Entry points raíz que también se eliminarán:'));
271
- for (const f of existingRootFiles) {
272
- console.log(` ${chalk.red('•')} ${f}`);
273
- }
274
- }
275
-
276
- console.log('');
277
-
278
- if (!options.force) {
279
- const { confirm } = await inquirer.prompt([{
280
- type: 'confirm',
281
- name: 'confirm',
282
- message: chalk.red(`⚠️ ¿Eliminar todos los archivos de LMAgent de este proyecto? Esta acción no se puede deshacer.`),
283
- default: false
284
- }]);
285
- if (!confirm) {
286
- console.log(chalk.gray('Cancelado.'));
287
- return;
288
- }
289
- }
290
-
291
- console.log('');
292
- let removed = 0;
293
- let errors = 0;
294
-
295
- // Eliminar directorios y archivos de cada agente
296
- for (const ide of installedIdes) {
297
- const toRemove = [];
298
-
299
- // Directorios del agente (skills, rules, workflows) — solo si son específicos del agente
300
- // No eliminar directorios genéricos como .github, rules/, skills/ que pueden tener otros usos
301
- const agentSpecificDirs = [ide.skillsDir, ide.workflowsDir];
302
- if (ide.rulesDir && !['rules', '.github/instructions'].includes(ide.rulesDir)) {
303
- agentSpecificDirs.push(ide.rulesDir);
304
- }
305
-
306
- // Determinar el directorio raíz del agente (ej: .cursor, .windsurf, .claude)
307
- const agentRootDir = ide.markerFile && !ide.markerFile.includes('.') ? null
308
- : ide.rulesDir ? ide.rulesDir.split('/')[0] : null;
309
- if (agentRootDir && agentRootDir.startsWith('.') && fs.existsSync(path.join(projectRoot, agentRootDir))) {
310
- // Eliminar el directorio raíz completo del agente
311
- toRemove.push({ path: path.join(projectRoot, agentRootDir), type: 'dir', label: agentRootDir + '/' });
312
- } else {
313
- // Eliminar subdirectorios individualmente
314
- for (const dir of agentSpecificDirs) {
315
- if (dir && fs.existsSync(path.join(projectRoot, dir))) {
316
- toRemove.push({ path: path.join(projectRoot, dir), type: 'dir', label: dir + '/' });
317
- }
318
- }
319
- }
320
-
321
- // Archivos de configuración específicos del agente (markerFile si es un archivo)
322
- if (ide.markerFile && ide.markerFile.includes('.') && fs.existsSync(path.join(projectRoot, ide.markerFile))) {
323
- const markerStat = fs.statSync(path.join(projectRoot, ide.markerFile));
324
- if (markerStat.isFile()) {
325
- toRemove.push({ path: path.join(projectRoot, ide.markerFile), type: 'file', label: ide.markerFile });
326
- }
327
- }
328
-
329
- for (const item of toRemove) {
330
- try {
331
- if (item.type === 'dir') {
332
- fs.rmSync(item.path, { recursive: true, force: true });
333
- } else {
334
- fs.unlinkSync(item.path);
335
- }
336
- console.log(` ${chalk.red('✘')} ${ide.name}: ${chalk.gray(item.label)} eliminado`);
337
- removed++;
338
- } catch (e) {
339
- console.error(` ${chalk.red('❌')} Error eliminando ${item.label}: ${e.message}`);
340
- errors++;
341
- }
342
- }
343
- }
344
-
345
- // Eliminar entry points raíz si --all
346
- if (options.all) {
347
- for (const f of existingRootFiles) {
348
- try {
349
- fs.unlinkSync(path.join(projectRoot, f));
350
- console.log(` ${chalk.red('✘')} ${chalk.gray(f)} eliminado`);
351
- removed++;
352
- } catch (e) {
353
- console.error(` ${chalk.red('❌')} Error eliminando ${f}: ${e.message}`);
354
- errors++;
355
- }
356
- }
357
- }
358
-
359
- console.log('');
360
- if (errors === 0) {
361
- console.log(gradient.pastel(`\n✨ Limpieza completada. ${removed} elemento(s) eliminado(s).\n`));
362
- if (!options.all) {
363
- console.log(chalk.dim(' 💡 Usa `lmagent uninstall --all` para también eliminar CLAUDE.md, GEMINI.md y AGENTS.md.'));
364
- }
365
- } else {
366
- console.log(chalk.yellow(`\n⚠️ ${removed} eliminado(s), ${errors} error(es).\n`));
367
- }
368
- });
369
-
370
- if (process.argv.length === 2) {
371
- runInstall({});
372
- } else {
373
- program.parse();
374
- }
375
-
376
- function arePathsEqual(p1, p2) {
377
- if (!p1 || !p2) return false;
378
- return path.resolve(p1).toLowerCase() === path.resolve(p2).toLowerCase();
379
- }
380
-
381
- // Helper to deploy AGENTS.md, CLAUDE.md y GEMINI.md to project root
382
- // Los archivos con versionTemplate:true tienen {{VERSION}} que se reemplaza dinámicamente
383
- async function deployCorePillars(options, projectRoot) {
384
- console.log(chalk.bold('\n🚀 Desplegando Pilares de Inteligencia (Contexto Root):'));
385
- for (const file of INIT_FILES) {
386
- const srcPath = path.join(__dirname, file.src);
387
- const destPath = path.join(projectRoot, file.src);
388
-
389
- if (fs.existsSync(srcPath)) {
390
- let shouldCopy = false;
391
- if (!fs.existsSync(destPath)) {
392
- shouldCopy = true;
393
- console.log(` ${chalk.green('✔')} ${file.src} (Creado en la raíz)`);
394
- } else {
395
- // Si ya existe pero tiene versión vieja, actualizar automáticamente
396
- const existingContent = fs.readFileSync(destPath, 'utf8');
397
- const hasOldVersion = existingContent.includes('{{VERSION}}') ||
398
- (file.versionTemplate && !existingContent.includes(`v${PKG_VERSION}`));
399
-
400
- if (options.force || hasOldVersion) {
401
- shouldCopy = true;
402
- const reason = hasOldVersion ? 'Actualizando versión' : 'Sobrescribiendo por --force';
403
- console.log(` ${chalk.yellow('✎')} ${file.src} (${reason})`);
404
- } else if (options.yes) {
405
- console.log(` ${chalk.blue('ℹ')} ${file.src} ya existe v${PKG_VERSION} (OK)`);
406
- } else {
407
- const answer = await inquirer.prompt([{
408
- type: 'confirm',
409
- name: 'overwrite',
410
- message: `⚠️ ${file.src} ya existe. ¿Deseas actualizarlo?`,
411
- default: false
412
- }]);
413
- shouldCopy = answer.overwrite;
414
- if (shouldCopy) console.log(` ${chalk.yellow('✎')} ${file.src} (Actualizado)`);
415
- }
416
- }
417
-
418
- if (shouldCopy) {
419
- let content = fs.readFileSync(srcPath, 'utf8');
420
- // Inyectar versión dinámica si el archivo usa template
421
- if (file.versionTemplate) {
422
- content = content.replace(/\{\{VERSION\}\}/g, PKG_VERSION);
423
- }
424
- fs.writeFileSync(destPath, content, 'utf8');
425
- }
426
- }
427
- }
428
- }
429
-
430
- async function runInstall(options) {
431
- console.clear();
432
- const branding = figlet.textSync('LMAGENT', { font: 'ANSI Shadow' });
433
- console.log(gradient.pastel.multiline(branding));
434
- console.log(gradient.cristal(' by QuBit\n'));
435
-
436
- const projectRoot = process.cwd();
437
- const userHome = os.homedir();
438
- const globalAgentDir = path.join(userHome, '.agents');
439
- const globalSkillsDir = path.join(globalAgentDir, 'skills');
440
- const globalRulesDir = path.join(globalAgentDir, 'rules');
441
- const globalWorkflowsDir = path.join(globalAgentDir, 'workflows');
442
-
443
- // Sync global SOLO si se pasa --global explícitamente (evita doble lectura de contexto)
444
- if (options.global) {
445
- console.log(chalk.blue('🌐 Sincronizando al repositorio global (~/.agents/)...'));
446
- try {
447
- if (!fs.existsSync(globalAgentDir)) fs.mkdirSync(globalAgentDir, { recursive: true });
448
- if (fs.existsSync(PACKAGE_SKILLS_DIR)) copyRecursiveSync(PACKAGE_SKILLS_DIR, globalSkillsDir, true);
449
- if (fs.existsSync(PACKAGE_RULES_DIR)) copyRecursiveSync(PACKAGE_RULES_DIR, globalRulesDir, true);
450
- if (fs.existsSync(PACKAGE_WORKFLOWS_DIR)) copyRecursiveSync(PACKAGE_WORKFLOWS_DIR, globalWorkflowsDir, true);
451
- if (fs.existsSync(PACKAGE_MEMORY_DIR)) copyRecursiveSync(PACKAGE_MEMORY_DIR, path.join(globalAgentDir, 'memory'), true);
452
- console.log(chalk.green('✔ Repositorio global sincronizado.'));
453
- } catch (e) {
454
- console.error(chalk.red(`❌ Error al sincronizar repositorio global: ${e.message}`));
455
- }
456
- }
457
-
458
- await deployCorePillars(options, projectRoot);
459
- const SOURCE_SKILLS = PACKAGE_SKILLS_DIR;
460
- const SOURCE_RULES = PACKAGE_RULES_DIR;
461
- const SOURCE_WORKFLOWS = PACKAGE_WORKFLOWS_DIR;
462
- const SOURCE_MEMORY = PACKAGE_MEMORY_DIR;
463
-
464
- let targetIdes = [];
465
- let selectedSkills = [];
466
- let selectedRules = [];
467
- let selectedWorkflows = []; // New
468
- let installMethod = 'symlink';
469
- let installTarget = 'project';
470
- let targetRoot = projectRoot;
471
-
472
- if (options.yes) {
473
- console.log(chalk.yellow('⚡ Modo: No interactivo'));
474
- targetIdes = IDE_CONFIGS.filter(ide =>
475
- ide.value !== 'custom' && (fs.existsSync(path.join(projectRoot, ide.rulesDir ? ide.rulesDir.split('/')[0] : '')) || (ide.markerFile && fs.existsSync(path.join(projectRoot, ide.markerFile))))
476
- );
477
- if (targetIdes.length === 0 && options.force) {
478
- targetIdes = [IDE_CONFIGS.find(i => i.value === 'cursor')];
479
- }
480
- selectedSkills = getAllItems(SOURCE_SKILLS, true);
481
- selectedRules = getAllItems(SOURCE_RULES, false);
482
- selectedWorkflows = getAllItems(SOURCE_WORKFLOWS, false);
483
- } else {
484
- console.log(chalk.gray('================================================================'));
485
- console.log(chalk.cyan('🔹 Configuración de Instalación'));
486
- console.log(chalk.gray('================================================================'));
487
-
488
- // UX OPTIMIZATION: "Project First" & Windows Compat
489
- // 1. Detect Environment
490
- const isWindows = os.platform() === 'win32';
491
- installMethod = isWindows ? 'copy' : 'symlink';
492
- installTarget = 'project';
493
- targetRoot = projectRoot;
494
-
495
- // 2. Banner simplified
496
- console.log(`📍 Destino: ${chalk.green('Proyecto Actual')}`);
497
- console.log(`🔧 Método: ${chalk.green(isWindows ? 'Copy (Windows Safe)' : 'Symlink (Live Updates)')}`);
498
- console.log('');
499
-
500
- // 3. Auto-Detect IDEs
501
- // Mapa de rutas de instalación global por agente (donde el IDE instala sus archivos en ~/)
502
- // HOME_PATHS: rutas de instalación global de cada agente en el sistema del usuario
503
- // Cubre los 37 agentes soportados. Se verifica si alguna de las rutas existe en ~/
504
- const HOME_PATHS = {
505
- // IDEs principales
506
- 'cursor': ['.cursor'],
507
- 'windsurf': ['.windsurf'],
508
- 'cline': ['.vscode/extensions', '.cline'],
509
- 'roo': ['.vscode/extensions', '.roo'],
510
- 'vscode': ['.vscode'],
511
- 'trae': ['.trae'],
512
- 'trae-cn': ['.trae-cn', '.trae'],
513
- 'claude': ['.claude'],
514
- 'zed': ['.config/zed', '.zed'],
515
- // Agentes CLI & autónomos
516
- 'antigravity': ['.gemini/antigravity'],
517
- 'gemini': ['.gemini'],
518
- 'augment': ['.augment'],
519
- 'continue': ['.continue'],
520
- 'codex': ['.codex'],
521
- 'goose': ['.config/goose', '.goose'],
522
- 'junie': ['.junie'],
523
- 'kilo': ['.kilocode'],
524
- 'kiro': ['.kiro'],
525
- 'opencode': ['.opencode'],
526
- 'openhands': ['.openhands'],
527
- 'amp': ['.agents'],
528
- 'zencoder': ['.zencoder'],
529
- 'codebuddy': ['.codebuddy'],
530
- 'crush': ['.crush'],
531
- 'droid': ['.factory'],
532
- 'mux': ['.mux'],
533
- 'qwen': ['.qwen'],
534
- // Agentes menores / nicho
535
- 'openclaw': ['.config/openclaw', '.openclaw'],
536
- 'command-code': ['.commandcode'],
537
- 'iflow': ['.iflow'],
538
- 'kode': ['.kode'],
539
- 'mcpjam': ['.mcpjam'],
540
- 'mistral': ['.vibe', '.mistral'],
541
- 'pi': ['.pi'],
542
- 'qoder': ['.qoder'],
543
- 'neovate': ['.neovate'],
544
- 'pochi': ['.pochi'],
545
- 'adal': ['.adal'],
546
- };
547
-
548
- const detectedIdes = IDE_CONFIGS.filter(ide => {
549
- if (ide.value === 'custom' || ide.value === 'generic') return false;
550
-
551
- // Check Project Root: usar markerFile primero (más preciso), luego rulesDir como fallback
552
- const markerInProject = ide.markerFile && fs.existsSync(path.join(projectRoot, ide.markerFile));
553
- const rulesDirInProject = ide.rulesDir && fs.existsSync(path.join(projectRoot, ide.rulesDir));
554
- const skillsDirInProject = ide.skillsDir && fs.existsSync(path.join(projectRoot, ide.skillsDir));
555
- const inProject = markerInProject || rulesDirInProject || skillsDirInProject;
556
-
557
- // Check User Home: usar mapa explícito de rutas de instalación global
558
- const homePaths = HOME_PATHS[ide.value] || [];
559
- const inHome = homePaths.some(p => fs.existsSync(path.join(userHome, p)));
560
-
561
- return inProject || inHome;
562
- });
563
- // 4. Smart Prompt
564
- let defaultChoice = detectedIdes.length > 0 ? detectedIdes.map(i => i.value) : ['cursor']; // Default to Cursor if nothing found
565
-
566
- console.log(chalk.gray('--- Selección de Agentes ---'));
567
- const ideAnswer = await inquirer.prompt([{
568
- type: 'checkbox',
569
- name: 'ides',
570
- message: '¿Para qué Agentes instalar?',
571
- choices: IDE_CONFIGS.map(ide => {
572
- const isDetected = detectedIdes.some(d => d.value === ide.value);
573
- return {
574
- name: ide.name + (isDetected ? chalk.green(' (Detectado)') : ''),
575
- value: ide.value,
576
- checked: isDetected
577
- };
578
- })
579
- }]);
580
-
581
- if (ideAnswer.ides.length === 0) {
582
- console.log(chalk.yellow('⚠️ Ningún IDE seleccionado. Saliendo...'));
583
- return;
584
- }
585
-
586
- targetIdes = [];
587
- for (const ideValue of ideAnswer.ides) {
588
- if (ideValue === 'custom') {
589
- const customPath = await inquirer.prompt([{
590
- type: 'input',
591
- name: 'path',
592
- message: 'Ingresa la ruta de Rules:',
593
- validate: (input) => input.length > 0 ? true : 'Requerido'
594
- }, {
595
- type: 'input',
596
- name: 'skillsPath',
597
- message: 'Ingresa la ruta de Skills (Opcional):'
598
- }, {
599
- type: 'input',
600
- name: 'workflowsPath',
601
- message: 'Ingresa la ruta de Workflows (Opcional):'
602
- }]);
603
- targetIdes.push({
604
- name: 'Custom',
605
- value: 'custom',
606
- rulesDir: customPath.path,
607
- skillsDir: customPath.skillsPath || customPath.path,
608
- workflowsDir: customPath.workflowsPath || customPath.path // fallback to rules
609
- });
610
- } else {
611
- targetIdes.push(IDE_CONFIGS.find(i => i.value === ideValue));
612
- }
613
- }
614
-
615
- const availableSkills = getAllItems(SOURCE_SKILLS, true);
616
- const availableRules = getAllItems(SOURCE_RULES, false);
617
- const availableWorkflows = getAllItems(SOURCE_WORKFLOWS, false);
618
- // Memory logic: usually just a directory, not individual items to select, but we can check if it exists
619
- const hasMemory = fs.existsSync(path.join(SOURCE_SKILLS, '../memory')); // Hacky relative check or use defined constant if available in scope
620
-
621
- console.log('');
622
- const quickInstall = await inquirer.prompt([{
623
- type: 'confirm',
624
- name: 'all',
625
- message: '⚡ Instalación Rápida: ¿Instalar TODO (Skills, Rules, Workflows, Memory)?',
626
- default: true
627
- }]);
628
-
629
- if (quickInstall.all) {
630
- selectedSkills = availableSkills;
631
- selectedRules = availableRules;
632
- selectedWorkflows = availableWorkflows;
633
- options.installMemory = true; // Flag to install memory
634
- } else {
635
- // Manual selection...
636
- // Seleccionar Skills
637
- console.log(chalk.bold('\n🔹 Skills Disponibles:'));
638
- const skillsAnswer = await inquirer.prompt([
639
- {
640
- type: 'checkbox',
641
- name: 'skills',
642
- message: 'Selecciona (Espacio para elegir, Enter para confirmar):',
643
- choices: availableSkills.map(s => ({ name: s, checked: true })),
644
- pageSize: 15
645
- }
646
- ]);
647
- selectedSkills = skillsAnswer.skills;
648
-
649
- // Seleccionar Rules
650
- console.log(chalk.bold('\n🔹 Reglas Disponibles:'));
651
- const rulesAnswer = await inquirer.prompt([
652
- {
653
- type: 'checkbox',
654
- name: 'rules',
655
- message: 'Selecciona (Espacio para elegir, Enter para confirmar):',
656
- choices: availableRules.map(r => ({ name: r, checked: true })),
657
- pageSize: 15
658
- }
659
- ]);
660
- selectedRules = rulesAnswer.rules;
661
-
662
- // Seleccionar Workflows
663
- console.log(chalk.bold('\n🔹 Workflows Disponibles:'));
664
- const workflowsAnswer = await inquirer.prompt([
665
- {
666
- type: 'checkbox',
667
- name: 'workflows',
668
- message: 'Selecciona (Espacio para elegir, Enter para confirmar):',
669
- choices: availableWorkflows.map(w => ({ name: w, checked: true })),
670
- pageSize: 15
671
- }
672
- ]);
673
- selectedWorkflows = workflowsAnswer.workflows;
674
-
675
- // Seleccionar Memory
676
- console.log(chalk.bold('\n🔹 Memoria (Contexto):'));
677
- const memoryAnswer = await inquirer.prompt([
678
- {
679
- type: 'confirm',
680
- name: 'memory',
681
- message: '¿Instalar estructura de Memoria (.agents/memory)?',
682
- default: true
683
- }
684
- ]);
685
- options.installMemory = memoryAnswer.memory;
686
- }
687
-
688
- // Opción global: sincronizar también a ~/.agents/
689
- console.log('');
690
- const globalAnswer = await inquirer.prompt([{
691
- type: 'confirm',
692
- name: 'global',
693
- message: '🌐 ¿También sincronizar al repositorio global (~/.agents/)? (Útil para Gemini CLI, Codex y agentes CLI)',
694
- default: false
695
- }]);
696
- if (globalAnswer.global) options.global = true;
697
-
698
- console.log('');
699
- const { confirm } = await inquirer.prompt([{
700
- type: 'confirm',
701
- name: 'confirm',
702
- message: '¿Proceder con la instalación?',
703
- default: true
704
- }]);
705
- if (!confirm) return;
706
- }
707
-
708
- console.log('');
709
- for (const ide of targetIdes) {
710
- let currentInstallMethod = installMethod;
711
- if (ide.forceCopy && currentInstallMethod === 'symlink') {
712
- console.log(chalk.yellow(`⚠️ ${ide.name} detectado: Forzando método 'copy' (Mejor compatibilidad)`));
713
- currentInstallMethod = 'copy';
714
- }
715
-
716
-
717
- if (selectedSkills.length > 0 && ide.skillsDir) {
718
- const targetDir = path.join(targetRoot, ide.skillsDir);
719
-
720
- // OPTIMIZATION: If target is Global Repo, we entered "Global Sync" mode
721
- if (arePathsEqual(targetDir, globalSkillsDir)) {
722
- console.log(chalk.blue(`\n ℹ ${ide.name}: Skills updated via Global Sync`));
723
- } else {
724
- console.log(chalk.bold(`\nInstalling Skills to ${chalk.cyan(targetDir)}:`));
725
-
726
- try {
727
- if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
728
-
729
- for (const skill of selectedSkills) {
730
- const srcFolder = path.join(SOURCE_SKILLS, skill);
731
- const destFolder = path.join(targetDir, skill);
732
-
733
- if (fs.existsSync(srcFolder)) {
734
- await applyFile(srcFolder, destFolder, currentInstallMethod);
735
- console.log(` ${chalk.green('✔')} ${skill}/`);
736
- }
737
- }
738
- } catch (e) {
739
- console.error(chalk.red(`❌ Error installing skills for ${ide.name}: ${e.message}`));
740
- }
741
- }
742
- }
743
-
744
- // 4. Generate/Update Global Config File (Bootstrap)
745
- let bootstrapStatus = 'SKIP';
746
- if (ide.configFile) {
747
- // Safety: Don't inject Markdown into JSON/YAML
748
- if (ide.configFile.endsWith('.json') || ide.configFile.endsWith('.yaml') || ide.configFile.endsWith('.yml')) {
749
- // console.log(chalk.gray(` ℹ Skipping bootstrap for ${ide.name} (Structured Config)`));
750
- bootstrapStatus = 'SKIP';
751
- } else {
752
- const configPath = path.join(targetRoot, ide.configFile);
753
- const relativeRulesPath = getRelLink(ide.configFile, '.agents/rules/00-master.md');
754
- const relativeCatalogPath = getRelLink(ide.configFile, 'AGENTS.md');
755
- const relativeContextPath = getRelLink(ide.configFile, 'CLAUDE.md');
756
-
757
- // Usar template específico del agente si existe, si no usar contenido genérico
758
- const AGENT_CONFIGS_TEMPLATE_DIR = path.join(__dirname, '.agents', 'templates', 'agent-configs');
759
- const templateFile = ide.configTemplate
760
- ? path.join(AGENT_CONFIGS_TEMPLATE_DIR, ide.configTemplate)
761
- : path.join(AGENT_CONFIGS_TEMPLATE_DIR, '_generic.md');
762
-
763
- let content;
764
- if (fs.existsSync(templateFile) && !ide.configFile.endsWith('.json')) {
765
- // Usar template del archivo, inyectando VERSION
766
- content = fs.readFileSync(templateFile, 'utf8')
767
- .replace(/\{\{VERSION\}\}/g, PKG_VERSION)
768
- .replace(/\{\{MAJOR\}\}/g, PKG_VERSION.split('.')[0]);
769
- } else if (ide.configFile.endsWith('.json') && fs.existsSync(templateFile)) {
770
- // JSON: usar template y reemplazar VERSION
771
- content = fs.readFileSync(templateFile, 'utf8')
772
- .replace(/\{\{VERSION\}\}/g, PKG_VERSION)
773
- .replace(/\{\{MAJOR\}\}/g, PKG_VERSION.split('.')[0]);
774
- } else {
775
- // Fallback: contenido genérico dinámico
776
- content = `
777
- # 🤖 LMAgent Framework v${PKG_VERSION}
778
- > Contexto Activo: Este proyecto utiliza el estándar LMAgent V${PKG_VERSION.split('.')[0]}.
779
-
780
- ## 🚨 SOURCE OF TRUTH (CEREBRO)
781
- **TU CONTEXTO Y REGLAS VIVEN AQUÍ 👉 [AGENTS.md](${relativeCatalogPath})**
782
- *Lee este archivo INMEDIATAMENTE para obtener tu identidad, skills y reglas operativas.*
783
-
784
- ## QUICK START TRIGGERS (Menu Rápido)
785
- Use estos comandos para activar su rol. Para detalles, consulte \`AGENTS.md\`.
786
-
787
- | Trigger | Rol / Skill | Objetivo |
788
- |:--- |:--- |:--- |
789
- | \`/orch\` | **Orchestrator** | Clasificar y delegar. |
790
- | \`/dev\` | **Backend** | APIs y Lógica. |
791
- | \`/front\` | **Frontend** | UI/UX, React. |
792
- | \`/pm\` | **Product** | PRDs y Roadmap. |
793
- | \`/fix\` | **Debugger** | Análisis de bugs. |
794
- | \`/arch\` | **Architect** | Diseño de sistemas. |
795
-
796
- !! SYSTEM NOTE: Read AGENTS.md to understand how to execute these roles. !!
797
- `;
798
- }
799
- // If file exists, check if we need to append
800
- try {
801
- if (fs.existsSync(configPath)) {
802
- // Check if it's a directory (Edge case: Cline legacy folders)
803
- if (fs.statSync(configPath).isDirectory()) {
804
- console.error(chalk.red(` ❌ Cannot bootstrap ${ide.configFile}: Is a directory.`));
805
- bootstrapStatus = 'ERROR';
806
- } else {
807
- const existingContent = fs.readFileSync(configPath, 'utf8');
808
- if (!existingContent.includes('QUICK START TRIGGERS')) {
809
- fs.appendFileSync(configPath, '\n' + content);
810
- bootstrapStatus = 'UPDATED';
811
- } else {
812
- bootstrapStatus = 'OK';
813
- }
814
- }
815
- } else {
816
- // Create parent dir if needed (for .github/copilot... etc)
817
- if (!fs.existsSync(path.dirname(configPath))) fs.mkdirSync(path.dirname(configPath), { recursive: true });
818
- fs.writeFileSync(configPath, content);
819
- bootstrapStatus = 'CREATED';
820
- }
821
- } catch (e) {
822
- console.error(chalk.red(` ❌ Error bootstrapping ${ide.name}: ${e.message}`));
823
- bootstrapStatus = 'ERROR';
824
- }
825
- }
826
- }
827
-
828
- if (bootstrapStatus !== 'SKIP' && bootstrapStatus !== 'OK') {
829
- console.log(` ${bootstrapStatus === 'CREATED' ? chalk.green('✔') : chalk.blue('ℹ')} ${ide.name} Bootstrap: ${bootstrapStatus}`);
830
- }
831
-
832
- // 4.1 Generate Bridge Rule
833
- // Si el agente NO tiene configFile, necesita un archivo puente en rulesDir para auto-invocarse.
834
- // Si tampoco tiene bridgeFile definido, usamos '00-lmagent.md' como default genérico.
835
- const bridgeFile = ide.bridgeFile || (ide.rulesDir && !ide.configFile ? '00-lmagent.md' : null);
836
- const needsBridge = bridgeFile && !ide.configFile;
837
- // Garantizar que rulesDir existe siempre (para que el agente pueda detectar la instalación)
838
- if (ide.rulesDir && !fs.existsSync(path.join(targetRoot, ide.rulesDir))) {
839
- fs.mkdirSync(path.join(targetRoot, ide.rulesDir), { recursive: true });
840
- }
841
- if (ide.rulesDir && needsBridge) {
842
- const bridgePath = path.join(targetRoot, ide.rulesDir, bridgeFile);
843
- const relativeBridgeToRoot = path.join(ide.rulesDir, bridgeFile);
844
- const relContext = getRelLink(relativeBridgeToRoot, 'CLAUDE.md');
845
- const relCatalog = getRelLink(relativeBridgeToRoot, 'AGENTS.md');
846
- const relRules = getRelLink(relativeBridgeToRoot, '.agents/rules/00-master.md');
847
-
848
- let bridgeContent = '';
849
-
850
- if (bridgeFile.endsWith('.mdc')) {
851
- // Cursor MDC Format
852
- bridgeContent = `---
853
- description: LMAgent Framework Entry Point - Use this rule to understand how to interact with the project skills and rules.
854
- globs: **/*
855
- ---
856
-
857
- # 🤖 LMAgent Bridge Rule
858
-
859
- Este proyecto está potenciado por **LMAgent v${PKG_VERSION}**.
860
-
861
- ## 🚨 SOURCE OF TRUTH (CEREBRO)
862
- **TU CONTEXTO Y REGLAS VIVEN AQUÍ 👉 [AGENTS.md](${relCatalog})**
863
- *Lee este archivo INMEDIATAMENTE para obtener tu identidad, skills y reglas operativas.*
864
-
865
- ## QUICK START TRIGGERS (Menu Rápido)
866
- Use estos comandos para activar su rol. Para detalles, consulte \`AGENTS.md\`.
867
-
868
- | Trigger | Rol / Skill | Objetivo |
869
- |:--- |:--- |:--- |
870
- | \`/orch\` | **Orchestrator** | Clasificar y delegar. |
871
- | \`/dev\` | **Backend** | APIs y Lógica. |
872
- | \`/front\` | **Frontend** | UI/UX, React. |
873
- | \`/pm\` | **Product** | PRDs y Roadmap. |
874
- | \`/fix\` | **Debugger** | Análisis de bugs. |
875
- | \`/arch\` | **Architect** | Diseño de sistemas. |
876
-
877
- !! SYSTEM NOTE: Read AGENTS.md to understand how to execute these roles. !!
878
- `;
879
- } else {
880
- // Standard Markdown (Universal & Cline/Windsurf)
881
- bridgeContent = `# 🤖 LMAgent Framework Entry Point
882
-
883
- Este proyecto utiliza **LMAgent v${PKG_VERSION}**.
884
-
885
- ## 🚨 SOURCE OF TRUTH (CEREBRO)
886
- **TU CONTEXTO Y REGLAS VIVEN AQUÍ 👉 [AGENTS.md](${relCatalog})**
887
- *Lee este archivo INMEDIATAMENTE para obtener tu identidad, skills y reglas operativas.*
888
-
889
- ## QUICK START TRIGGERS (Menu Rápido)
890
- Use estos comandos para activar su rol. Para detalles, consulte \`AGENTS.md\`.
891
-
892
- | Trigger | Rol / Skill | Objetivo |
893
- |:--- |:--- |:--- |
894
- | \`/orch\` | **Orchestrator** | Clasificar y delegar. |
895
- | \`/dev\` | **Backend** | APIs y Lógica. |
896
- | \`/front\` | **Frontend** | UI/UX, React. |
897
- | \`/pm\` | **Product** | PRDs y Roadmap. |
898
- | \`/fix\` | **Debugger** | Análisis de bugs. |
899
- | \`/arch\` | **Architect** | Diseño de sistemas. |
900
- `;
901
- }
902
-
903
- // CLEANUP: Legacy Cursor Skills Directory
904
- // Since we moved skills to .cursor/rules/skills, we must remove .cursor/skills to avoid duplicates
905
- if (ide.value === 'cursor') {
906
- const legacySkillsDir = path.join(targetRoot, '.cursor/skills');
907
- if (fs.existsSync(legacySkillsDir)) {
908
- try {
909
- fs.rmSync(legacySkillsDir, { recursive: true, force: true });
910
- console.log(` ${chalk.yellow('🗑 Eliminado directorio obsoleto:')} .cursor / skills(Movido a.cursor / rules / skills)`);
911
- } catch (e) {
912
- console.error(chalk.red(` ⚠️ No se pudo eliminar.cursor / skills: ${e.message} `));
913
- }
914
- }
915
- }
916
-
917
- try {
918
- if (!fs.existsSync(path.dirname(bridgePath))) fs.mkdirSync(path.dirname(bridgePath), { recursive: true });
919
- fs.writeFileSync(bridgePath, bridgeContent);
920
- console.log(` ${chalk.green('✔')} ${ide.name} Bridge Rule: ${bridgeFile} `);
921
- } catch (e) {
922
- console.error(chalk.red(` ❌ Error creating bridge for ${ide.name}: ${e.message} `));
923
- }
924
- }
925
- // 2. Install RULES (Files)
926
- if (selectedRules.length > 0 && ide.rulesDir) {
927
- const targetDir = path.join(targetRoot, ide.rulesDir);
928
-
929
- // OPTIMIZATION: If target is Global Repo, skip redundant copy
930
- if (arePathsEqual(targetDir, globalRulesDir)) {
931
- console.log(chalk.blue(` ℹ ${ide.name}: Rules updated via Global Sync`));
932
- } else {
933
- console.log(chalk.bold(`\nInstalling Rules to ${chalk.cyan(targetDir)}: `));
934
-
935
- try {
936
- if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
937
-
938
-
939
- // CLEANUP: Remove legacy rules (V2)
940
- // CLEANUP: Remove legacy rules (V2 & Duplicates)
941
- const legacyRules = [
942
- '_bootstrap.md', '_bootstrap.mdc', '00-bootstrap.md',
943
- 'agents-ia.md', 'stack.md', 'testing.md', 'security.md', 'code-style.md', 'documentation.md',
944
- 'workflow.md', 'api-design.md', 'automations-n8n.md', 'frontend.md', 'backend.md'
945
- ];
946
- for (const legacy of legacyRules) {
947
- const legacyPath = path.join(targetDir, legacy);
948
- if (fs.existsSync(legacyPath)) {
949
- fs.unlinkSync(legacyPath);
950
- console.log(` ${chalk.yellow('🗑 Eliminado regla obsoleta:')} ${legacy} `);
951
- }
952
- }
953
-
954
- for (const rule of selectedRules) {
955
- const srcVal = path.join(SOURCE_RULES, rule);
956
- const destVal = path.join(targetDir, rule);
957
-
958
- if (fs.existsSync(srcVal)) {
959
- await applyFile(srcVal, destVal, currentInstallMethod);
960
- console.log(` ${chalk.blue('✔')} ${rule} `);
961
- }
962
- }
963
- } catch (e) {
964
- console.error(chalk.red(`❌ Error installing rules for ${ide.name}: ${e.message} `));
965
- }
966
- }
967
- }
968
-
969
- // 3. Install WORKFLOWS (Files)
970
- if (selectedWorkflows.length > 0 && ide.workflowsDir) {
971
- const targetDir = path.join(targetRoot, ide.workflowsDir);
972
-
973
- // OPTIMIZATION: If target is Global Repo, skip redundant copy
974
- if (arePathsEqual(targetDir, globalWorkflowsDir)) {
975
- console.log(chalk.blue(` ℹ ${ide.name}: Workflows updated via Global Sync`));
976
- } else {
977
- console.log(chalk.bold(`\nInstalling Workflows to ${chalk.cyan(targetDir)}: `));
978
-
979
- try {
980
- if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
981
-
982
- for (const wf of selectedWorkflows) {
983
- const srcVal = path.join(SOURCE_WORKFLOWS, wf);
984
- const destVal = path.join(targetDir, wf);
985
-
986
- if (fs.existsSync(srcVal)) {
987
- await applyFile(srcVal, destVal, currentInstallMethod);
988
- console.log(` ${chalk.magenta('✔')} ${wf} `);
989
- }
990
- }
991
- } catch (e) {
992
- console.error(chalk.red(`❌ Error installing workflows for ${ide.name}: ${e.message} `));
993
- }
994
- }
995
- }
996
-
997
-
998
-
999
- if (SOURCE_MEMORY && ide.skillsDir) {
1000
- // We use skillsDir parent or a specific memory dir if we had one in config.
1001
- // For now, let's put it alongside skills/rules/workflows.
1002
- // Ideally IDE_CONFIGS should have memoryDir, but we'll default to parent of skillsDir + /memory
1003
- const parentDir = path.dirname(ide.skillsDir);
1004
- const targetDir = path.join(targetRoot, parentDir, 'memory');
1005
-
1006
- if (arePathsEqual(targetDir, path.join(globalAgentDir, 'memory'))) {
1007
- // console.log(chalk.blue(` ℹ ${ ide.name }: Memory updated via Global Sync`));
1008
- } else {
1009
- // console.log(chalk.bold(`\nInstalling Memory to ${ chalk.cyan(targetDir) }: `));
1010
- try {
1011
- if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
1012
- // Copy all contents of memory
1013
- copyRecursiveSync(SOURCE_MEMORY, targetDir, true); // Always copy/overwrite for now, or use applyFile for items if we want symlinks
1014
- console.log(` ${chalk.cyan('✔')} Memory(Context) optimized.`);
1015
- } catch (e) {
1016
- console.error(chalk.red(`❌ Error installing memory for ${ide.name}: ${e.message} `));
1017
- }
1018
- }
1019
- }
1020
- }
1021
- console.log(gradient.pastel.multiline('\n✨ Instalación Finalizada '));
1022
-
1023
- console.log(chalk.gray('================================================================'));
1024
- console.log(chalk.bold.green('🎉 ¡Todo listo! Aquí tienes cómo usar tus nuevos superpoderes:'));
1025
- console.log('');
1026
-
1027
- // Mensaje dinámico según agentes instalados
1028
- const ideNames = targetIdes.map(i => i.name).join(', ');
1029
- console.log(chalk.cyan(`🤖 Agentes configurados: ${chalk.bold(ideNames)} `));
1030
- console.log('');
1031
- console.log(chalk.white(' 1. Abre tu agente en este proyecto — leerá el contexto automáticamente.'));
1032
- console.log(chalk.white(' 2. Usa los triggers para activar un rol específico:'));
1033
- console.log(chalk.gray(' /dev → Backend | /front → Frontend | /arch → Arquitecto'));
1034
- console.log(chalk.gray(' /fix → Debugger | /pm → Product | /orch → Orchestrator'));
1035
- console.log('');
1036
- console.log(chalk.dim(' 💡 Ejecuta `lmagent doctor` para verificar la instalación.'));
1037
- console.log(chalk.dim(' 💡 Ejecuta `lmagent tokens` para ver el consumo de tokens del framework.'));
1038
- console.log(chalk.gray('================================================================'));
1039
- }
1040
-
1041
- async function applyFile(source, dest, method) {
1042
- const srcPath = path.resolve(source);
1043
- const destPath = path.resolve(dest);
1044
-
1045
- // Case-insensitive check for Windows compatibility
1046
- if (srcPath.toLowerCase() === destPath.toLowerCase()) {
1047
- // console.log(chalk.gray(` (Skipping self - install: ${ path.basename(source) })`));
1048
- return;
1049
- }
1050
- if (fs.existsSync(dest) || (fs.existsSync(path.dirname(dest)) && fs.readdirSync(path.dirname(dest)).includes(path.basename(dest)))) {
1051
- try {
1052
- const stat = fs.statSync(dest);
1053
- if (stat.isDirectory()) {
1054
- fs.rmSync(dest, { recursive: true, force: true });
1055
- } else {
1056
- fs.unlinkSync(dest);
1057
- }
1058
- } catch (e) { }
1059
- }
1060
-
1061
- const destDir = path.dirname(dest);
1062
- if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
1063
-
1064
- const srcStat = fs.statSync(source);
1065
- const isDir = srcStat.isDirectory();
1066
-
1067
- if (method === 'symlink') {
1068
- try {
1069
- const type = isDir ? 'junction' : 'file';
1070
- fs.symlinkSync(source, dest, type);
1071
- } catch (e) {
1072
- try {
1073
- if (isDir) {
1074
- copyRecursiveSync(source, dest, true);
1075
- } else {
1076
- fs.copyFileSync(source, dest);
1077
- }
1078
- const isWindows = os.platform() === 'win32';
1079
- const msg = isWindows && !isDir
1080
- ? `(Symlink falló[Requiere Admin / DevMode en Win].Copiado.)`
1081
- : `(Symlink falló, se usó copia)`;
1082
- console.log(chalk.yellow(` ${msg} `));
1083
- } catch (err) {
1084
- console.error(chalk.red(` Error copiando ${path.basename(dest)}: ${err.message} `));
1085
- }
1086
- }
1087
- } else {
1088
- if (isDir) {
1089
- copyRecursiveSync(source, dest, true);
1090
- } else {
1091
- fs.copyFileSync(source, dest);
1092
- }
1093
- }
1094
- }
1095
-
1096
- function copyRecursiveSync(src, dest, overwrite) {
1097
- if (fs.cpSync) {
1098
- try {
1099
- fs.cpSync(src, dest, { recursive: true, force: overwrite, errorOnExist: false });
1100
- } catch (e) {
1101
- console.error(chalk.red(`Error copying(cpSync) ${path.basename(src)}: ${e.message} `));
1102
- // Fallback manual implementation just in case
1103
- if (fs.existsSync(src)) {
1104
- const stat = fs.statSync(src);
1105
- if (stat.isDirectory()) {
1106
- if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
1107
- fs.readdirSync(src).forEach(child => {
1108
- copyRecursiveSync(path.join(src, child), path.join(dest, child), overwrite);
1109
- });
1110
- } else {
1111
- fs.copyFileSync(src, dest);
1112
- }
1113
- }
1114
- }
1115
- } else {
1116
- // Fallback for older Node versions
1117
- const exists = fs.existsSync(src);
1118
- const stats = exists && fs.statSync(src);
1119
- const isDirectory = exists && stats.isDirectory();
1120
- if (isDirectory) {
1121
- if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
1122
- fs.readdirSync(src).forEach(function (childItemName) {
1123
- copyRecursiveSync(path.join(src, childItemName),
1124
- path.join(dest, childItemName), overwrite);
1125
- });
1126
- } else {
1127
- if (overwrite || !fs.existsSync(dest)) {
1128
- fs.copyFileSync(src, dest);
1129
- }
1130
- }
1131
- }
1132
- }
1133
-
1134
- function getAllItems(dir, isNested) {
1135
- if (!fs.existsSync(dir)) return [];
1136
- const items = fs.readdirSync(dir);
1137
- if (isNested) {
1138
- return items.filter(item => {
1139
- const p = path.join(dir, item);
1140
- return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'SKILL.md'));
1141
- });
1142
- } else {
1143
- return items.filter(item => item.endsWith('.md') || item.endsWith('.txt') || item.endsWith('.cursorrules') || item.endsWith('.toml'));
1144
- }
1145
- }
1146
-
1147
- // ============================================
1148
- // INIT: Inicializar proyecto con LMAgent
1149
- // ============================================
1150
-
1151
- async function runInit(options) {
1152
- let targetIdes = []; // Initialize targetIdes
1153
- console.clear();
1154
- const branding = figlet.textSync('LMAGENT', { font: 'ANSI Shadow' });
1155
- console.log(gradient.pastel.multiline(branding));
1156
- console.log(gradient.cristal(' by QuBit\n'));
1157
-
1158
- const projectRoot = process.cwd();
1159
- const targetRoot = projectRoot; // Fix for ReferenceError in runInit
1160
- console.log(chalk.cyan(`📦 Inicializando proyecto LMAgent en: ${chalk.bold(projectRoot)} \n`));
1161
-
1162
- // Verificar si ya está inicializado
1163
- const agentsExists = fs.existsSync(path.join(projectRoot, 'AGENTS.md'));
1164
- if (agentsExists && !options.force) {
1165
- console.log(chalk.yellow('⚠️ Este proyecto ya tiene AGENTS.md'));
1166
- if (!options.yes) {
1167
- const { overwrite } = await inquirer.prompt([{
1168
- type: 'confirm',
1169
- name: 'overwrite',
1170
- message: '¿Sobrescribir archivos existentes?',
1171
- default: false
1172
- }]);
1173
- if (!overwrite) {
1174
- console.log(chalk.yellow('Cancelado. Usa --force para forzar.'));
1175
- return;
1176
- }
1177
- }
1178
- }
1179
-
1180
- let filesToCopy = [...INIT_FILES];
1181
- let dirsToCopy = [...INIT_DIRS];
1182
-
1183
- // Modo interactivo: preguntar qué copiar
1184
- if (!options.yes) {
1185
- const answers = await inquirer.prompt([
1186
- {
1187
- type: 'checkbox',
1188
- name: 'files',
1189
- message: 'Archivos de entry point a copiar:',
1190
- choices: INIT_FILES.map(f => ({
1191
- name: `${f.src} - ${f.desc} `,
1192
- value: f.src,
1193
- checked: true
1194
- }))
1195
- },
1196
- {
1197
- type: 'checkbox',
1198
- name: 'dirs',
1199
- message: 'Directorios a copiar:',
1200
- choices: INIT_DIRS.map(d => ({
1201
- name: `${d.src}/ - ${d.desc}`,
1202
- value: d.src,
1203
- checked: true
1204
- }))
1205
- }
1206
- ]);
1207
- dirsToCopy = INIT_DIRS.filter(d => answers.dirs.includes(d.src));
1208
-
1209
- // Seleccionar IDE para destino de archivos
1210
- console.log('');
1211
- const ideAnswer = await inquirer.prompt([
1212
- {
1213
- type: 'checkbox',
1214
- name: 'ides',
1215
- message: 'Selecciona tu IDE principal (para ubicar las carpetas):',
1216
- choices: IDE_CONFIGS.filter(i => i.value !== 'custom').map(i => ({
1217
- name: i.name,
1218
- value: i.value,
1219
- checked: i.value === 'cursor'
1220
- }))
1221
- }
1222
- ]);
1223
- targetIdes = IDE_CONFIGS.filter(i => ideAnswer.ides.includes(i.value));
1224
- } else {
1225
- // Defaults for non-interactive
1226
- targetIdes = [IDE_CONFIGS.find(i => i.value === 'cursor')];
1227
- }
1228
-
1229
- // Copiar archivos del framework a la carpeta del Agente (Clean Root)
1230
- console.log(chalk.bold('\n📦 Instalando framework en directorios de Agente:'));
1231
-
1232
- for (const ide of targetIdes) {
1233
- if (!ide.skillsDir) continue; // Skip custom/manual if no dir defined
1234
-
1235
- // Determinar "Agent Root" (ej: .cursor/ o .github/)
1236
- // Asume que skillsDir es "root/skills", así que dirname obtiene "root"
1237
- const agentRootDir = path.join(targetRoot, path.dirname(ide.skillsDir));
1238
-
1239
- console.log(chalk.dim(` Destino: ${agentRootDir}`));
1240
-
1241
- // Crear directorio root si no existe
1242
- if (!fs.existsSync(agentRootDir)) fs.mkdirSync(agentRootDir, { recursive: true });
1243
-
1244
- // 5. Install Root Configs (CLAUDE.md, AGENTS.md) with Prompt
1245
- console.log(chalk.bold('\nChecking Root Configurations:'));
1246
- for (const file of INIT_FILES) {
1247
- const srcPath = path.join(__dirname, file.src);
1248
- const destPath = path.join(targetRoot, file.src);
1249
-
1250
- if (fs.existsSync(srcPath)) {
1251
- if (!fs.existsSync(destPath)) {
1252
- let content = fs.readFileSync(srcPath, 'utf8');
1253
- if (file.versionTemplate) content = content.replace(/\{\{VERSION\}\}/g, PKG_VERSION);
1254
- fs.writeFileSync(destPath, content, 'utf8');
1255
- console.log(` ${chalk.green('✔')} ${file.src} (Created)`);
1256
- } else {
1257
- // Exists: Ask to overwrite (unless force/yes)
1258
- let shouldOverwrite = false;
1259
- if (options.force) {
1260
- shouldOverwrite = true;
1261
- } else if (!options.yes) {
1262
- const answer = await inquirer.prompt([{
1263
- type: 'confirm',
1264
- name: 'overwrite',
1265
- message: `⚠️ ${file.src} ya existe. ¿Sobrescribir?`,
1266
- default: false
1267
- }]);
1268
- shouldOverwrite = answer.overwrite;
1269
- }
1270
-
1271
- if (shouldOverwrite) {
1272
- let content = fs.readFileSync(srcPath, 'utf8');
1273
- if (file.versionTemplate) content = content.replace(/\{\{VERSION\}\}/g, PKG_VERSION);
1274
- fs.writeFileSync(destPath, content, 'utf8');
1275
- console.log(` ${chalk.yellow('✎')} ${file.src} (Overwritten)`);
1276
- } else {
1277
- console.log(` ${chalk.gray('SKIP')} ${file.src} (Kept existing)`);
1278
- }
1279
- }
1280
- }
1281
- }
1282
-
1283
- // Copiar Directorios (docs, config, templates)
1284
- for (const dir of dirsToCopy) {
1285
- const src = path.join(__dirname, dir.src);
1286
- const dest = path.join(agentRootDir, dir.src);
1287
- if (fs.existsSync(src)) {
1288
- copyRecursiveSync(src, dest, true); // Force overwrite
1289
- console.log(` ${chalk.green('✔')} ${dir.src}/ -> ${path.dirname(ide.skillsDir)}/${dir.src}/`);
1290
-
1291
- // CLEANUP: If docs, remove assets (legacy logo, etc.)
1292
- if (dir.src === 'docs') {
1293
- const assetsDir = path.join(dest, 'assets');
1294
- if (fs.existsSync(assetsDir)) {
1295
- try {
1296
- fs.rmSync(assetsDir, { recursive: true, force: true });
1297
- console.log(` ${chalk.yellow('🗑 Eliminado assets heredados (logo, etc.)')}`);
1298
- } catch (e) { }
1299
- }
1300
- }
1301
- }
1302
- }
1303
- }
1304
-
1305
- // Crear .env.example si no existe
1306
- const envExampleDest = path.join(projectRoot, '.env.example');
1307
- if (!fs.existsSync(envExampleDest)) {
1308
- const envContent = `# ============================================
1309
- # LMAgent Project - Environment Variables
1310
- # ============================================
1311
- # Copiar este archivo a .env y completar los valores
1312
-
1313
- # Database
1314
- DATABASE_URL=postgresql://user:password@localhost:5432/dbname
1315
-
1316
- # Redis
1317
- REDIS_URL=redis://localhost:6379
1318
-
1319
- # Security
1320
- JWT_SECRET=change-this-to-a-strong-secret-minimum-32-characters
1321
-
1322
- # LLM API Keys
1323
- OPENAI_API_KEY=sk-...
1324
- ANTHROPIC_API_KEY=sk-ant-...
1325
- GOOGLE_API_KEY=...
1326
-
1327
- # n8n (si aplica)
1328
- N8N_WEBHOOK_URL=https://n8n.yourserver.com/webhook
1329
-
1330
- # Environment
1331
- ENVIRONMENT=development
1332
- DEBUG=true
1333
- `;
1334
- fs.writeFileSync(envExampleDest, envContent);
1335
- console.log(` ${chalk.green('✔')} .env.example ${chalk.green('(nuevo)')}`);
1336
- } else {
1337
- console.log(` ${chalk.blue('ℹ')} .env.example ya existe, no se sobrescribe`);
1338
- }
1339
-
1340
- // Resumen
1341
- console.log(gradient.pastel.multiline(`\n✨ Proyecto inicializado con LMAgent v${PKG_VERSION} ✨`));
1342
- console.log('');
1343
- console.log(chalk.cyan('Próximos pasos:'));
1344
- console.log(` 1. ${chalk.bold('lmagent install')} - Instalar skills/rules/workflows en tu IDE`);
1345
- console.log(` 2. Editar ${chalk.bold('.env.example')} → ${chalk.bold('.env')} con tus credenciales`);
1346
- console.log(` 3. Leer ${chalk.bold('AGENTS.md')} para conocer las capacidades disponibles`);
1347
- console.log('');
1348
-
1349
- // Preguntar si quiere ejecutar install también
1350
- if (!options.yes) {
1351
- const { runInstallNow } = await inquirer.prompt([{
1352
- type: 'confirm',
1353
- name: 'runInstallNow',
1354
- message: '¿Ejecutar lmagent install ahora para conectar al IDE?',
1355
- default: true
1356
- }]);
1357
- if (runInstallNow) {
1358
- console.log('');
1359
- await runInstall(options);
1360
- }
1361
- } else {
1362
- console.log(chalk.cyan('💡 Ejecuta `lmagent install` para conectar al IDE.\n'));
1363
- }
1364
- }
1365
-
1366
- // ============================================
1367
- // DOCTOR: Verificar configuración del proyecto
1368
- // ============================================
1369
-
1370
- async function runDoctor() {
1371
- console.clear();
1372
- const branding = figlet.textSync('LMAGENT', { font: 'ANSI Shadow' });
1373
- console.log(gradient.pastel.multiline(branding));
1374
- console.log(gradient.cristal(' Doctor - Diagnóstico\n'));
1375
-
1376
- const projectRoot = process.cwd();
1377
- let issues = 0;
1378
- let ok = 0;
1379
-
1380
- console.log(chalk.bold('🔍 Verificando proyecto en: ' + chalk.cyan(projectRoot) + '\n'));
1381
-
1382
- // 1. Archivos de entry point
1383
- console.log(chalk.bold('📄 Entry Points:'));
1384
- for (const file of INIT_FILES) {
1385
- const exists = fs.existsSync(path.join(projectRoot, file.src));
1386
- if (exists) {
1387
- console.log(` ${chalk.green('✔')} ${file.src}`);
1388
- ok++;
1389
- } else {
1390
- console.log(` ${chalk.red('')} ${file.src} - ${chalk.red('FALTANTE')} ejecuta ${chalk.bold('lmagent init')}`);
1391
- issues++;
1392
- }
1393
- }
1394
-
1395
- // 2. Directorios de configuración
1396
- console.log(chalk.bold('\n📁 Configuración:'));
1397
- for (const dir of INIT_DIRS) {
1398
- const exists = fs.existsSync(path.join(projectRoot, dir.src));
1399
- if (exists) {
1400
- console.log(` ${chalk.green('')} ${dir.src}/`);
1401
- ok++;
1402
- } else {
1403
- console.log(` ${chalk.yellow('⚠')} ${dir.src}/ - Opcional, ejecuta ${chalk.bold('lmagent init')} para copiar`);
1404
- }
1405
- }
1406
-
1407
- // 3. Detectar IDEs configurados
1408
- console.log(chalk.bold('\n🔧 IDEs detectados:'));
1409
- let ideFound = false;
1410
- for (const ide of IDE_CONFIGS) {
1411
- if (ide.value === 'custom') continue;
1412
- const rulesExist = ide.rulesDir && fs.existsSync(path.join(projectRoot, ide.rulesDir));
1413
- const skillsExist = ide.skillsDir && fs.existsSync(path.join(projectRoot, ide.skillsDir));
1414
- const markerExist = ide.markerFile && fs.existsSync(path.join(projectRoot, ide.markerFile));
1415
-
1416
- if (rulesExist || skillsExist || markerExist) {
1417
- ideFound = true;
1418
- const parts = [];
1419
- if (rulesExist) parts.push('rules');
1420
- if (skillsExist) parts.push('skills');
1421
- console.log(` ${chalk.green('✔')} ${ide.name} (${parts.join(', ')})`);
1422
- ok++;
1423
-
1424
- // Verificar que tiene los skills correctos
1425
- if (skillsExist) {
1426
- const installedSkills = fs.readdirSync(path.join(projectRoot, ide.skillsDir))
1427
- .filter(item => fs.statSync(path.join(projectRoot, ide.skillsDir, item)).isDirectory());
1428
-
1429
- // Calcular skills esperados dinámicamente
1430
- const expectedSkillsCount = getAllItems(PACKAGE_SKILLS_DIR, true).length;
1431
- const skillCount = installedSkills.length;
1432
-
1433
- if (skillCount < expectedSkillsCount) {
1434
- console.log(` ${chalk.yellow('⚠')} Solo ${skillCount}/${expectedSkillsCount} skills instalados → ejecuta ${chalk.bold('lmagent install')}`);
1435
- } else {
1436
- console.log(` ${chalk.green('✔')} ${skillCount} skills instalados`);
1437
- }
1438
- }
1439
- }
1440
- }
1441
- if (!ideFound) {
1442
- console.log(` ${chalk.red('✘')} Ningún IDE detectado → ejecuta ${chalk.bold('lmagent install')}`);
1443
- issues++;
1444
- }
1445
-
1446
- // 4. Verificar .env
1447
- console.log(chalk.bold('\n🔒 Seguridad:'));
1448
- const envExists = fs.existsSync(path.join(projectRoot, '.env'));
1449
- const envExampleExists = fs.existsSync(path.join(projectRoot, '.env.example'));
1450
- const gitignoreExists = fs.existsSync(path.join(projectRoot, '.gitignore'));
1451
-
1452
- if (envExampleExists) {
1453
- console.log(` ${chalk.green('✔')} .env.example existe`);
1454
- ok++;
1455
- } else {
1456
- console.log(` ${chalk.yellow('⚠')} .env.example no encontrado`);
1457
- }
1458
-
1459
- if (envExists) {
1460
- console.log(` ${chalk.green('✔')} .env existe`);
1461
- ok++;
1462
- } else {
1463
- console.log(` ${chalk.yellow('⚠')} .env no encontrado (necesario para ejecutar)`);
1464
- }
1465
-
1466
- if (gitignoreExists) {
1467
- const gitignore = fs.readFileSync(path.join(projectRoot, '.gitignore'), 'utf-8');
1468
- if (gitignore.includes('.env')) {
1469
- console.log(` ${chalk.green('✔')} .env está en .gitignore`);
1470
- ok++;
1471
- } else {
1472
- console.log(` ${chalk.red('✘')} .env NO está en .gitignore → ${chalk.red('RIESGO DE SEGURIDAD')}`);
1473
- issues++;
1474
- }
1475
- }
1476
-
1477
- // Resumen
1478
- console.log('');
1479
- if (issues === 0) {
1480
- console.log(gradient.pastel(`\n✨ Todo en orden! ${ok} verificaciones pasadas.\n`));
1481
- } else {
1482
- console.log(chalk.yellow(`\n⚠️ ${issues} problema(s) encontrado(s), ${ok} verificaciones OK.\n`));
1483
- }
1484
- }
1485
-
1486
- // Helper: Calculate relative Markdown link
1487
- function getRelLink(fromRelPath, toRelPath) {
1488
- const fromDir = path.dirname(fromRelPath);
1489
- let rel = path.relative(fromDir, toRelPath);
1490
- if (!rel.startsWith('.')) rel = './' + rel;
1491
- return rel.replace(/\\/g, '/'); // Force forward slashes for Markdown
1492
- }
1493
-
1494
- // Helper: Contar archivos recursivamente
1495
- function getAllItemsFlat(dir) {
1496
- let results = [];
1497
- if (!fs.existsSync(dir)) return results;
1498
- const items = fs.readdirSync(dir);
1499
- for (const item of items) {
1500
- const fullPath = path.join(dir, item);
1501
- if (fs.statSync(fullPath).isDirectory()) {
1502
- results = results.concat(getAllItemsFlat(fullPath));
1503
- } else {
1504
- results.push(fullPath);
1505
- }
1506
- }
1507
- return results;
1508
- }
1509
-
1510
-
1511
- // Execute CLI only if run directly
1512
- if (require.main === module) {
1513
- if (process.argv.length === 2) {
1514
- runInstall({});
1515
- } else {
1516
- program.parse(process.argv);
1517
- }
1518
- }
1519
-
1520
- module.exports = { IDE_CONFIGS };
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { Command } = require('commander');
7
+ const chalk = require('chalk');
8
+ const inquirer = require('inquirer');
9
+ const figlet = require('figlet');
10
+ const gradient = require('gradient-string');
11
+
12
+ const program = new Command();
13
+ const PKG_VERSION = require('./package.json').version;
14
+
15
+ // Configuración: Directorios fuente del paquete
16
+ const PACKAGE_SKILLS_DIR = path.join(__dirname, '.agents', 'skills');
17
+ const PACKAGE_RULES_DIR = path.join(__dirname, '.agents', 'rules');
18
+ const PACKAGE_WORKFLOWS_DIR = path.join(__dirname, '.agents', 'workflows');
19
+ const PACKAGE_CONFIG_DIR = path.join(__dirname, '.agents', 'config');
20
+ const PACKAGE_TEMPLATES_DIR = path.join(__dirname, '.agents', 'templates');
21
+ const PACKAGE_DOCS_DIR = path.join(__dirname, '.agents', 'docs');
22
+ const PACKAGE_MEMORY_DIR = path.join(__dirname, '.agents', 'memory');
23
+
24
+ // Archivos de proyecto que init copia a la raíz
25
+ // Usan {{VERSION}} como placeholder; se reemplaza dinámicamente al instalar
26
+ const INIT_FILES = [
27
+ { src: 'AGENTS.md', desc: 'Catálogo de capacidades LMAgent (Entry Point Universal)', versionTemplate: false },
28
+ // CLAUDE.md y GEMINI.md NO van aquí se despliegan solo cuando su agente está detectado/seleccionado,
29
+ // para evitar conflictos de contexto duplicado en agentes como Cursor y Zed que leen múltiples .md del raíz.
30
+ ];
31
+
32
+ const INIT_DIRS = [
33
+ { src: 'config', desc: 'Configuración del framework' },
34
+ { src: 'templates', desc: 'Templates de proyecto' },
35
+ { src: 'docs', desc: 'Documentación extendida' },
36
+ { src: 'workflows', desc: 'SOPs y Procedimientos' },
37
+ ];
38
+
39
+ // IDE_CONFIGS: Lista ÚNICA y DEDUPLICADA de todos los agentes soportados
40
+ const IDE_CONFIGS = [
41
+ // --- IDEs Principales (Auto-Detectados) ---
42
+ // Cursor: usa .cursor/rules/*.mdc (formato MDC con frontmatter)
43
+ { name: 'Cursor', value: 'cursor', rulesDir: '.cursor/rules', skillsDir: '.cursor/rules/skills', workflowsDir: '.cursor/workflows', configFile: null, bridgeFile: '00-lmagent.mdc', markerFile: '.cursorrules', forceCopy: true },
44
+ // Windsurf Wave 8+: usa .windsurf/rules/*.md
45
+ { name: 'Windsurf', value: 'windsurf', rulesDir: '.windsurf/rules', skillsDir: '.windsurf/skills', workflowsDir: '.windsurf/workflows', configFile: null, bridgeFile: 'lmagent.md', markerFile: '.windsurf', forceCopy: true },
46
+ // Cline: usa .clinerules/ (directorio con .md files)
47
+ { name: 'Cline', value: 'cline', rulesDir: '.clinerules', skillsDir: '.cline/skills', workflowsDir: '.cline/workflows', configFile: null, bridgeFile: '00-lmagent.md', markerFile: '.clinerules', forceCopy: true },
48
+ // Roo Code: usa .roo/rules/
49
+ { name: 'Roo Code', value: 'roo', rulesDir: '.roo/rules', skillsDir: '.roo/skills', workflowsDir: '.roo/workflows', configFile: null, bridgeFile: '00-lmagent.md', markerFile: '.roo', forceCopy: true },
50
+ // GitHub Copilot: usa .github/copilot-instructions.md + .github/instructions/*.md
51
+ { name: 'VSCode Copilot', value: 'vscode', rulesDir: '.github/instructions', skillsDir: '.github/skills', workflowsDir: '.github/copilot-workflows', configFile: '.github/copilot-instructions.md', bridgeFile: null, markerFile: '.vscode' },
52
+ { name: 'Trae', value: 'trae', rulesDir: '.trae/rules', skillsDir: '.trae/skills', workflowsDir: '.trae/workflows', configFile: null, bridgeFile: 'lmagent.md', markerFile: '.trae', forceCopy: true },
53
+ // Claude Code: usa .claude/
54
+ { name: 'Claude Code', value: 'claude', rulesDir: '.claude/rules', skillsDir: '.claude/skills', workflowsDir: '.claude/workflows', configFile: 'CLAUDE.md', bridgeFile: null, markerFile: '.claude', forceCopy: true },
55
+ { name: 'Zed', value: 'zed', rulesDir: '.rules', skillsDir: '.rules/skills', workflowsDir: '.rules/workflows', configFile: null, bridgeFile: 'lmagent.md', markerFile: '.zed' },
56
+
57
+ // --- Agentes CLI & Autónomos ---
58
+ { name: 'Amp / Kimi / Replit', value: 'amp', rulesDir: '.agents/rules', skillsDir: '.agents/skills', workflowsDir: '.agents/workflows', configFile: null, bridgeFile: null, markerFile: '.agents' },
59
+ // Antigravity (Google Deepmind)
60
+ { name: 'Antigravity', value: 'antigravity', rulesDir: '.agent/rules', skillsDir: '.agent/skills', workflowsDir: '.agent/workflows', configFile: 'GEMINI.md', bridgeFile: null, markerFile: '.agent' },
61
+ { name: 'Augment', value: 'augment', rulesDir: '.augment/rules', skillsDir: '.augment/skills', workflowsDir: '.augment/workflows', configFile: null, bridgeFile: null, markerFile: '.augment' },
62
+ // Gemini CLI
63
+ { name: 'Gemini CLI', value: 'gemini', rulesDir: '.gemini/rules', skillsDir: '.gemini/skills', workflowsDir: '.gemini/workflows', configFile: 'GEMINI.md', bridgeFile: null, markerFile: '.gemini' },
64
+ { name: 'OpenClaw / Envoid', value: 'openclaw', rulesDir: 'rules', skillsDir: 'skills', workflowsDir: 'workflows', configFile: 'openclaw.json', configTemplate: 'openclaw.json', bridgeFile: null, markerFile: 'openclaw.json' },
65
+ { name: 'CodeBuddy', value: 'codebuddy', rulesDir: '.codebuddy/rules', skillsDir: '.codebuddy/skills', workflowsDir: '.codebuddy/workflows', configFile: null, bridgeFile: null, markerFile: '.codebuddy', forceCopy: true },
66
+ // Codex CLI (OpenAI)
67
+ { name: 'Codex', value: 'codex', rulesDir: '.codex', skillsDir: '.codex/skills', workflowsDir: '.codex/workflows', configFile: 'AGENTS.md', bridgeFile: null, markerFile: '.codex' },
68
+ { name: 'Command Code', value: 'command-code', rulesDir: '.commandcode/rules', skillsDir: '.commandcode/skills', workflowsDir: '.commandcode/workflows', configFile: null, bridgeFile: null, markerFile: '.commandcode' },
69
+ // Continue
70
+ { name: 'Continue', value: 'continue', rulesDir: '.continue/rules', skillsDir: '.continue/skills', workflowsDir: '.continue/workflows', configFile: '.continue/continuerules', configTemplate: 'continuerules.md', bridgeFile: '00-lmagent.md', markerFile: '.continue' },
71
+ { name: 'Crush', value: 'crush', rulesDir: '.crush/rules', skillsDir: '.crush/skills', workflowsDir: '.crush/workflows', configFile: null, bridgeFile: null, markerFile: '.crush' },
72
+ { name: 'Droid', value: 'droid', rulesDir: '.factory/rules', skillsDir: '.factory/skills', workflowsDir: '.factory/workflows', configFile: null, bridgeFile: null, markerFile: '.factory' },
73
+ // Goose (Block)
74
+ // Goose: .goosehints va en la RAÍZ del proyecto (no en .goose/), según docs oficiales
75
+ { name: 'Goose', value: 'goose', rulesDir: '.goose', skillsDir: '.goose/skills', workflowsDir: '.goose/workflows', configFile: '.goosehints', configTemplate: 'goosehints.md', bridgeFile: null, markerFile: '.goose' },
76
+ // Junie (JetBrains)
77
+ { name: 'Junie', value: 'junie', rulesDir: '.junie', skillsDir: '.junie/skills', workflowsDir: '.junie/workflows', configFile: '.junie/guidelines.md', configTemplate: 'junie-guidelines.md', bridgeFile: null, markerFile: '.junie' },
78
+ { name: 'iFlow CLI', value: 'iflow', rulesDir: '.iflow/rules', skillsDir: '.iflow/skills', workflowsDir: '.iflow/workflows', configFile: null, bridgeFile: null, markerFile: '.iflow' },
79
+ { name: 'Kilo Code', value: 'kilo', rulesDir: '.kilocode/rules', skillsDir: '.kilocode/skills', workflowsDir: '.kilocode/workflows', configFile: null, bridgeFile: null, markerFile: '.kilocode' },
80
+ { name: 'Kiro CLI', value: 'kiro', rulesDir: '.kiro/rules', skillsDir: '.kiro/skills', workflowsDir: '.kiro/workflows', configFile: null, bridgeFile: null, markerFile: '.kiro' },
81
+ { name: 'Kode', value: 'kode', rulesDir: '.kode/rules', skillsDir: '.kode/skills', workflowsDir: '.kode/workflows', configFile: null, bridgeFile: null, markerFile: '.kode' },
82
+ { name: 'MCPJam', value: 'mcpjam', rulesDir: '.mcpjam/rules', skillsDir: '.mcpjam/skills', workflowsDir: '.mcpjam/workflows', configFile: null, bridgeFile: null, markerFile: '.mcpjam' },
83
+ { name: 'Mistral Vibe', value: 'mistral', rulesDir: '.vibe/rules', skillsDir: '.vibe/skills', workflowsDir: '.vibe/workflows', configFile: null, bridgeFile: null, markerFile: '.vibe' },
84
+ { name: 'Mux', value: 'mux', rulesDir: '.mux/rules', skillsDir: '.mux/skills', workflowsDir: '.mux/workflows', configFile: null, bridgeFile: null, markerFile: '.mux' },
85
+ { name: 'OpenCode', value: 'opencode', rulesDir: '.opencode/rules', skillsDir: '.opencode/skills', workflowsDir: '.opencode/workflows', configFile: null, bridgeFile: null, markerFile: '.opencode' },
86
+ // OpenHands: usa .openhands/microagents/repo.md
87
+ { name: 'OpenHands', value: 'openhands', rulesDir: '.openhands/microagents', skillsDir: '.openhands/skills', workflowsDir: '.openhands/workflows', configFile: '.openhands/microagents/repo.md', configTemplate: 'openhands-repo.md', bridgeFile: null, markerFile: '.openhands' },
88
+ { name: 'Pi', value: 'pi', rulesDir: '.pi/rules', skillsDir: '.pi/skills', workflowsDir: '.pi/workflows', configFile: null, bridgeFile: null, markerFile: '.pi' },
89
+ { name: 'Qoder', value: 'qoder', rulesDir: '.qoder/rules', skillsDir: '.qoder/skills', workflowsDir: '.qoder/workflows', configFile: null, bridgeFile: null, markerFile: '.qoder' },
90
+ { name: 'Qwen Code', value: 'qwen', rulesDir: '.qwen/rules', skillsDir: '.qwen/skills', workflowsDir: '.qwen/workflows', configFile: null, bridgeFile: null, markerFile: '.qwen' },
91
+ { name: 'Trae CN', value: 'trae-cn', rulesDir: '.trae-cn/rules', skillsDir: '.trae-cn/skills', workflowsDir: '.trae-cn/workflows', configFile: null, bridgeFile: 'lmagent.md', markerFile: '.trae-cn' },
92
+ { name: 'Zencoder', value: 'zencoder', rulesDir: '.zencoder/rules', skillsDir: '.zencoder/skills', workflowsDir: '.zencoder/workflows', configFile: null, bridgeFile: null, markerFile: '.zencoder' },
93
+ { name: 'Neovate', value: 'neovate', rulesDir: '.neovate/rules', skillsDir: '.neovate/skills', workflowsDir: '.neovate/workflows', configFile: null, bridgeFile: null, markerFile: '.neovate' },
94
+ { name: 'Pochi', value: 'pochi', rulesDir: '.pochi/rules', skillsDir: '.pochi/skills', workflowsDir: '.pochi/workflows', configFile: null, bridgeFile: null, markerFile: '.pochi' },
95
+ { name: 'AdaL', value: 'adal', rulesDir: '.adal/rules', skillsDir: '.adal/skills', workflowsDir: '.adal/workflows', configFile: null, bridgeFile: null, markerFile: '.adal' },
96
+
97
+ // --- Opciones Especiales ---
98
+ { name: 'Generic/Other', value: 'generic', rulesDir: '.agents/rules', skillsDir: '.agents/skills', workflowsDir: '.agents/workflows', configFile: null, bridgeFile: null, markerFile: '.agents' },
99
+ { name: 'Custom Path (Manual)', value: 'custom', rulesDir: '', skillsDir: '', workflowsDir: '', configFile: null, bridgeFile: null, markerFile: '' },
100
+ ];
101
+
102
+ program
103
+ .name('lmagent')
104
+ .description('CLI para instalar skills y reglas de LMAgent')
105
+ .version(PKG_VERSION);
106
+
107
+ program.command('install')
108
+ .description('Instalar skills, rules y workflows en el IDE del proyecto')
109
+ .option('-f, --force', 'Forzar instalación')
110
+ .option('-y, --yes', 'Instalar todo sin preguntar')
111
+ .action((options) => {
112
+ runInstall(options);
113
+ });
114
+
115
+ program.command('update')
116
+ .description('Actualizar skills y reglas en el proyecto (alias de install)')
117
+ .option('-f, --force', 'Forzar actualización')
118
+ .option('-y, --yes', 'Instalar todo sin preguntar')
119
+ .action((options) => {
120
+ console.log(chalk.blue('ℹ Iniciando actualización...'));
121
+ runInstall(options);
122
+ });
123
+
124
+ program.command('init')
125
+ .description('Inicializar proyecto con LMAgent (copia AGENTS.md y estructura base)')
126
+ .option('-f, --force', 'Sobrescribir archivos existentes')
127
+ .option('-y, --yes', 'No preguntar, instalar todo')
128
+ .action((options) => {
129
+ runInit(options);
130
+ });
131
+
132
+ program.command('doctor')
133
+ .description('Verificar que el proyecto está correctamente configurado')
134
+ .action(() => {
135
+ runDoctor();
136
+ });
137
+
138
+ program.command('validate')
139
+ .description('Validar integridad de todos los skills (frontmatter, estructura)')
140
+ .argument('[skill]', 'Nombre parcial del skill a validar (opcional)')
141
+ .action((skill) => {
142
+ const { execSync } = require('child_process');
143
+ const scriptPath = path.join(__dirname, 'scripts', 'validate_skills.js');
144
+ const args = skill ? ` ${skill}` : '';
145
+ try {
146
+ execSync(`node "${scriptPath}"${args}`, { stdio: 'inherit' });
147
+ } catch (e) {
148
+ process.exit(e.status || 1);
149
+ }
150
+ });
151
+
152
+ program.command('create-skill')
153
+ .description('Crear un nuevo skill interactivamente')
154
+ .action(() => {
155
+ const { execSync } = require('child_process');
156
+ const scriptPath = path.join(__dirname, 'scripts', 'create_skill.js');
157
+ try {
158
+ execSync(`node "${scriptPath}"`, { stdio: 'inherit' });
159
+ } catch (e) {
160
+ process.exit(e.status || 1);
161
+ }
162
+ });
163
+
164
+ program.command('tokens')
165
+ .description('Analizar consumo de tokens del framework instalado en el proyecto')
166
+ .option('--json', 'Salida en formato JSON')
167
+ .option('--report', 'Generar reporte en .agents/token-report.md')
168
+ .action((options) => {
169
+ const { execSync } = require('child_process');
170
+ const scriptPath = path.join(__dirname, 'scripts', 'token-analyzer.js');
171
+ const args = [options.json ? '--json' : '', options.report ? '--report' : ''].filter(Boolean).join(' ');
172
+ try {
173
+ execSync(`node "${scriptPath}" ${args}`, { stdio: 'inherit' });
174
+ } catch (e) {
175
+ process.exit(e.status || 1);
176
+ }
177
+ });
178
+
179
+ program.command('skills')
180
+ .description('Gestionar skills externos desde GitHub (compatible con el estándar skills.sh)')
181
+ .argument('<action>', 'Acción: add')
182
+ .argument('<source>', 'Repositorio GitHub: owner/repo o URL completa')
183
+ .option('--skill <name>', 'Nombre específico del skill a instalar (opcional)')
184
+ .action(async (action, source, opts) => {
185
+ if (action !== 'add') {
186
+ console.error(chalk.red(`❌ Acción desconocida: ${action}. Usa: lmagent skills add <owner/repo>`));
187
+ process.exit(1);
188
+ }
189
+ const { execSync } = require('child_process');
190
+ const repoSlug = source.replace('https://github.com/', '').replace(/\.git$/, '');
191
+ const [owner, repo] = repoSlug.split('/');
192
+ if (!owner || !repo) {
193
+ console.error(chalk.red('❌ Formato inválido. Usa: lmagent skills add owner/repo'));
194
+ process.exit(1);
195
+ }
196
+ const tmpDir = path.join(os.tmpdir(), `lmagent-skill-${Date.now()}`);
197
+ const targetSkillsDir = path.join(process.cwd(), '.agents', 'skills');
198
+ console.log(chalk.cyan(`📦 Descargando skill desde github.com/${owner}/${repo}...`));
199
+ try {
200
+ execSync(`git clone --depth 1 https://github.com/${owner}/${repo} "${tmpDir}"`, { stdio: 'pipe' });
201
+ const skillsPath = fs.existsSync(path.join(tmpDir, 'skills')) ? path.join(tmpDir, 'skills') : tmpDir;
202
+ const items = fs.readdirSync(skillsPath).filter(i => {
203
+ const p = path.join(skillsPath, i);
204
+ return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'SKILL.md'));
205
+ });
206
+ if (items.length === 0) {
207
+ console.log(chalk.yellow('⚠️ No se encontraron skills con SKILL.md en el repositorio.'));
208
+ fs.rmSync(tmpDir, { recursive: true, force: true });
209
+ return;
210
+ }
211
+ const toInstall = opts.skill ? items.filter(i => i.includes(opts.skill)) : items;
212
+ if (!fs.existsSync(targetSkillsDir)) fs.mkdirSync(targetSkillsDir, { recursive: true });
213
+ for (const skill of toInstall) {
214
+ const src = path.join(skillsPath, skill);
215
+ const dest = path.join(targetSkillsDir, skill);
216
+ copyRecursiveSync(src, dest, true);
217
+ console.log(` ${chalk.green('✔')} ${skill}/`);
218
+ }
219
+ fs.rmSync(tmpDir, { recursive: true, force: true });
220
+ console.log(chalk.green(`✨ ${toInstall.length} skill(s) instalado(s) en .agents/skills/`));
221
+ console.log(chalk.dim(' Ejecuta `lmagent install` para sincronizarlos a tu agente.'));
222
+ } catch (e) {
223
+ console.error(chalk.red(`❌ Error al instalar skill: ${e.message}`));
224
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { }
225
+ process.exit(1);
226
+ }
227
+ });
228
+
229
+ program.command('uninstall')
230
+ .description('Eliminar todos los archivos instalados por LMAgent del proyecto')
231
+ .option('-f, --force', 'No pedir confirmación, eliminar directamente')
232
+ .option('--all', 'También eliminar entry points raíz (CLAUDE.md, GEMINI.md, AGENTS.md)')
233
+ .action(async (options) => {
234
+ console.clear();
235
+ const branding = figlet.textSync('LMAGENT', { font: 'ANSI Shadow' });
236
+ console.log(gradient.pastel.multiline(branding));
237
+ console.log(gradient.cristal(' Uninstall - Limpieza\n'));
238
+
239
+ const projectRoot = process.cwd();
240
+
241
+ // Detectar qué agentes están instalados o dejaron rastro en el proyecto
242
+ // Al desinstalar, también incluimos 'generic' para volar .agents/
243
+ const installedIdes = IDE_CONFIGS.filter(ide => {
244
+ if (ide.value === 'custom') return false;
245
+ const markerInProject = ide.markerFile && fs.existsSync(path.join(projectRoot, ide.markerFile));
246
+ const rulesDirInProject = ide.rulesDir && fs.existsSync(path.join(projectRoot, ide.rulesDir));
247
+ const skillsDirInProject = ide.skillsDir && fs.existsSync(path.join(projectRoot, ide.skillsDir));
248
+ return markerInProject || rulesDirInProject || skillsDirInProject || ide.value === 'generic';
249
+ });
250
+
251
+ const rootEntryFiles = ['CLAUDE.md', 'GEMINI.md', 'AGENTS.md', '.cursorrules', '.windsurfrules', '.windsurfrules.md', '.continuerules', '.goosehints', 'openclaw.json'];
252
+ const existingRootFiles = rootEntryFiles.filter(f => fs.existsSync(path.join(projectRoot, f)));
253
+
254
+ if (installedIdes.length === 0 && existingRootFiles.length === 0) {
255
+ console.log(chalk.yellow('⚠️ No se detectó ningún rastro del framework en este proyecto.'));
256
+ return;
257
+ }
258
+
259
+ console.log(chalk.bold('🔍 Agentes detectados en este proyecto:\n'));
260
+ for (const ide of installedIdes) {
261
+ const parts = [];
262
+ if (ide.rulesDir && fs.existsSync(path.join(projectRoot, ide.rulesDir))) parts.push(chalk.gray(ide.rulesDir));
263
+ if (ide.skillsDir && fs.existsSync(path.join(projectRoot, ide.skillsDir))) parts.push(chalk.gray(ide.skillsDir));
264
+ if (ide.configFile && fs.existsSync(path.join(projectRoot, ide.configFile))) parts.push(chalk.gray(ide.configFile));
265
+ console.log(` ${chalk.cyan('•')} ${chalk.bold(ide.name)}: ${parts.join(', ')}`);
266
+ }
267
+
268
+ // Entry points raíz (ya calculados arriba como rootEntryFiles)
269
+
270
+ if (options.all && existingRootFiles.length > 0) {
271
+ console.log(chalk.bold('\n📄 Entry points raíz que también se eliminarán:'));
272
+ for (const f of existingRootFiles) {
273
+ console.log(` ${chalk.red('•')} ${f}`);
274
+ }
275
+ }
276
+
277
+ console.log('');
278
+
279
+ if (!options.force) {
280
+ const { confirm } = await inquirer.prompt([{
281
+ type: 'confirm',
282
+ name: 'confirm',
283
+ message: chalk.red(`⚠️ ¿Eliminar todos los archivos de LMAgent de este proyecto? Esta acción no se puede deshacer.`),
284
+ default: false
285
+ }]);
286
+ if (!confirm) {
287
+ console.log(chalk.gray('Cancelado.'));
288
+ return;
289
+ }
290
+ }
291
+
292
+ console.log('');
293
+ let removed = 0;
294
+ let errors = 0;
295
+
296
+ // Eliminar directorios y archivos de cada agente
297
+ for (const ide of installedIdes) {
298
+ const toRemove = [];
299
+
300
+ // Directorios del agente (skills, rules, workflows) solo si son específicos del agente
301
+ // No eliminar directorios genéricos como .github, rules/, skills/ que pueden tener otros usos
302
+ const agentSpecificDirs = [ide.skillsDir, ide.workflowsDir];
303
+ if (ide.rulesDir && !['rules', '.github/instructions'].includes(ide.rulesDir)) {
304
+ agentSpecificDirs.push(ide.rulesDir);
305
+ }
306
+ if (ide.value === 'generic') {
307
+ agentSpecificDirs.push('.agents');
308
+ }
309
+
310
+ // Determinar el directorio raíz del agente (ej: .cursor, .windsurf, .claude)
311
+ const agentRootDir = ide.markerFile && !ide.markerFile.includes('.') ? null
312
+ : ide.rulesDir ? ide.rulesDir.split('/')[0] : null;
313
+ if (agentRootDir && agentRootDir.startsWith('.') && fs.existsSync(path.join(projectRoot, agentRootDir))) {
314
+ // Eliminar el directorio raíz completo del agente
315
+ toRemove.push({ path: path.join(projectRoot, agentRootDir), type: 'dir', label: agentRootDir + '/' });
316
+ } else {
317
+ // Eliminar subdirectorios individualmente
318
+ for (const dir of agentSpecificDirs) {
319
+ if (dir && fs.existsSync(path.join(projectRoot, dir))) {
320
+ toRemove.push({ path: path.join(projectRoot, dir), type: 'dir', label: dir + '/' });
321
+ }
322
+ }
323
+ }
324
+
325
+ // Archivos de configuración específicos del agente (markerFile si es un archivo)
326
+ if (ide.markerFile && ide.markerFile.includes('.') && fs.existsSync(path.join(projectRoot, ide.markerFile))) {
327
+ const markerStat = fs.statSync(path.join(projectRoot, ide.markerFile));
328
+ if (markerStat.isFile()) {
329
+ toRemove.push({ path: path.join(projectRoot, ide.markerFile), type: 'file', label: ide.markerFile });
330
+ }
331
+ }
332
+
333
+ for (const item of toRemove) {
334
+ try {
335
+ if (item.type === 'dir') {
336
+ fs.rmSync(item.path, { recursive: true, force: true });
337
+ } else {
338
+ fs.unlinkSync(item.path);
339
+ }
340
+ console.log(` ${chalk.red('✘')} ${ide.name}: ${chalk.gray(item.label)} eliminado`);
341
+ removed++;
342
+ } catch (e) {
343
+ console.error(` ${chalk.red('❌')} Error eliminando ${item.label}: ${e.message}`);
344
+ errors++;
345
+ }
346
+ }
347
+ }
348
+
349
+ // Eliminar entry points raíz si --all
350
+ if (options.all) {
351
+ for (const f of existingRootFiles) {
352
+ try {
353
+ fs.unlinkSync(path.join(projectRoot, f));
354
+ console.log(` ${chalk.red('✘')} ${chalk.gray(f)} eliminado`);
355
+ removed++;
356
+ } catch (e) {
357
+ console.error(` ${chalk.red('❌')} Error eliminando ${f}: ${e.message}`);
358
+ errors++;
359
+ }
360
+ }
361
+ }
362
+
363
+ console.log('');
364
+ if (errors === 0) {
365
+ console.log(gradient.pastel(`\n✨ Limpieza completada. ${removed} elemento(s) eliminado(s).\n`));
366
+ if (!options.all) {
367
+ console.log(chalk.dim(' 💡 Usa `lmagent uninstall --all` para también eliminar CLAUDE.md, GEMINI.md y AGENTS.md.'));
368
+ }
369
+ } else {
370
+ console.log(chalk.yellow(`\n⚠️ ${removed} eliminado(s), ${errors} error(es).\n`));
371
+ }
372
+ });
373
+
374
+ // CLI entry point is handled at the bottom of the file (require.main === module)
375
+
376
+ function arePathsEqual(p1, p2) {
377
+ if (!p1 || !p2) return false;
378
+ return path.resolve(p1).toLowerCase() === path.resolve(p2).toLowerCase();
379
+ }
380
+
381
+ // Helper to deploy AGENTS.md, CLAUDE.md y GEMINI.md to project root
382
+ // Los archivos con versionTemplate:true tienen {{VERSION}} que se reemplaza dinámicamente
383
+ async function deployCorePillars(options, projectRoot) {
384
+ console.log(chalk.bold('\n🚀 Desplegando Pilares de Inteligencia (Contexto Root):'));
385
+ for (const file of INIT_FILES) {
386
+ const srcPath = path.join(__dirname, file.src);
387
+ const destPath = path.join(projectRoot, file.src);
388
+
389
+ if (fs.existsSync(srcPath)) {
390
+ let shouldCopy = false;
391
+ if (!fs.existsSync(destPath)) {
392
+ shouldCopy = true;
393
+ console.log(` ${chalk.green('✔')} ${file.src} (Creado en la raíz)`);
394
+ } else {
395
+ // Si ya existe pero tiene versión vieja, actualizar automáticamente
396
+ const existingContent = fs.readFileSync(destPath, 'utf8');
397
+ const hasOldVersion = existingContent.includes('{{VERSION}}') ||
398
+ (file.versionTemplate && !existingContent.includes(`v${PKG_VERSION}`));
399
+
400
+ if (options.force || hasOldVersion) {
401
+ shouldCopy = true;
402
+ const reason = hasOldVersion ? 'Actualizando versión' : 'Sobrescribiendo por --force';
403
+ console.log(` ${chalk.yellow('✎')} ${file.src} (${reason})`);
404
+ } else if (options.yes) {
405
+ console.log(` ${chalk.blue('ℹ')} ${file.src} ya existe v${PKG_VERSION} (OK)`);
406
+ } else {
407
+ const answer = await inquirer.prompt([{
408
+ type: 'confirm',
409
+ name: 'overwrite',
410
+ message: `⚠️ ${file.src} ya existe. ¿Deseas actualizarlo?`,
411
+ default: false
412
+ }]);
413
+ shouldCopy = answer.overwrite;
414
+ if (shouldCopy) console.log(` ${chalk.yellow('✎')} ${file.src} (Actualizado)`);
415
+ }
416
+ }
417
+
418
+ if (shouldCopy) {
419
+ let content = fs.readFileSync(srcPath, 'utf8');
420
+ // Inyectar versión dinámica si el archivo usa template
421
+ if (file.versionTemplate) {
422
+ content = content.replace(/\{\{VERSION\}\}/g, PKG_VERSION);
423
+ }
424
+ fs.writeFileSync(destPath, content, 'utf8');
425
+ }
426
+ }
427
+ }
428
+ }
429
+
430
+ async function runInstall(options) {
431
+ console.clear();
432
+ const branding = figlet.textSync('LMAGENT', { font: 'ANSI Shadow' });
433
+ console.log(gradient.pastel.multiline(branding));
434
+ console.log(gradient.cristal(' by QuBit\n'));
435
+
436
+ const projectRoot = process.cwd();
437
+
438
+ await deployCorePillars(options, projectRoot);
439
+ const SOURCE_SKILLS = PACKAGE_SKILLS_DIR;
440
+ const SOURCE_RULES = PACKAGE_RULES_DIR;
441
+ const SOURCE_WORKFLOWS = PACKAGE_WORKFLOWS_DIR;
442
+ const SOURCE_MEMORY = PACKAGE_MEMORY_DIR;
443
+
444
+ let targetIdes = [];
445
+ let selectedSkills = [];
446
+ let selectedRules = [];
447
+ let selectedWorkflows = []; // New
448
+ let installMethod = 'symlink';
449
+ let installTarget = 'project';
450
+ let targetRoot = projectRoot;
451
+
452
+ if (options.yes) {
453
+ console.log(chalk.yellow('⚡ Modo: No interactivo'));
454
+ targetIdes = IDE_CONFIGS.filter(ide =>
455
+ ide.value !== 'custom' && (fs.existsSync(path.join(projectRoot, ide.rulesDir ? ide.rulesDir.split('/')[0] : '')) || (ide.markerFile && fs.existsSync(path.join(projectRoot, ide.markerFile))))
456
+ );
457
+ if (targetIdes.length === 0 && options.force) {
458
+ targetIdes = [IDE_CONFIGS.find(i => i.value === 'cursor')];
459
+ }
460
+ selectedSkills = getAllItems(SOURCE_SKILLS, true);
461
+ selectedRules = getAllItems(SOURCE_RULES, false);
462
+ selectedWorkflows = getAllItems(SOURCE_WORKFLOWS, false);
463
+ } else {
464
+ console.log(chalk.gray('================================================================'));
465
+ console.log(chalk.cyan('🔹 Configuración de Instalación'));
466
+ console.log(chalk.gray('================================================================'));
467
+
468
+ // UX OPTIMIZATION: "Project First" & Windows Compat
469
+ // 1. Detect Environment
470
+ const isWindows = os.platform() === 'win32';
471
+ installMethod = isWindows ? 'copy' : 'symlink';
472
+ installTarget = 'project';
473
+ targetRoot = projectRoot;
474
+
475
+ // 2. Banner simplified
476
+ console.log(`📍 Destino: ${chalk.green('Proyecto Actual')}`);
477
+ console.log(`🔧 Método: ${chalk.green(isWindows ? 'Copy (Windows Safe)' : 'Symlink (Live Updates)')}`);
478
+ console.log('');
479
+
480
+ // 3. Auto-Detect IDEs — SOLO en el PROYECTO (no en HOME, para evitar falsos positivos)
481
+ const detectedIdes = IDE_CONFIGS.filter(ide => {
482
+ if (ide.value === 'custom' || ide.value === 'generic') return false;
483
+
484
+ const markerInProject = ide.markerFile && fs.existsSync(path.join(projectRoot, ide.markerFile));
485
+ const rulesDirInProject = ide.rulesDir && fs.existsSync(path.join(projectRoot, ide.rulesDir));
486
+ const skillsDirInProject = ide.skillsDir && fs.existsSync(path.join(projectRoot, ide.skillsDir));
487
+ // Detectar también por configFile existente en el proyecto
488
+ const configInProject = ide.configFile && fs.existsSync(path.join(projectRoot, ide.configFile));
489
+ return markerInProject || rulesDirInProject || skillsDirInProject || configInProject;
490
+ });
491
+
492
+ // 4. Smart Multi-Agent Auto-Detection Setup
493
+ if (detectedIdes.length === 0) {
494
+ console.log(chalk.yellow('⚠️ No se detectaron agentes instalados en este entorno.'));
495
+ console.log(chalk.blue('ℹ Instalando estructura estándar para Cursor por defecto.'));
496
+ targetIdes = [IDE_CONFIGS.find(i => i.value === 'cursor')];
497
+ } else {
498
+ console.log(chalk.green(`\n🚀 ¡Agentes Detectados! (${detectedIdes.length})`));
499
+ const names = detectedIdes.map(i => i.name).join(', ');
500
+ console.log(chalk.cyan(` → Se aplicará integración aislada para: ${chalk.bold(names)}\n`));
501
+ targetIdes = detectedIdes;
502
+ }
503
+
504
+ const availableSkills = getAllItems(SOURCE_SKILLS, true);
505
+ const availableRules = getAllItems(SOURCE_RULES, false);
506
+ const availableWorkflows = getAllItems(SOURCE_WORKFLOWS, false);
507
+ // Memory logic: usually just a directory, not individual items to select, but we can check if it exists
508
+ const hasMemory = fs.existsSync(path.join(SOURCE_SKILLS, '../memory')); // Hacky relative check or use defined constant if available in scope
509
+
510
+ console.log('');
511
+ const quickInstall = await inquirer.prompt([{
512
+ type: 'confirm',
513
+ name: 'all',
514
+ message: '⚡ Instalación Rápida: ¿Instalar TODO (Skills, Rules, Workflows, Memory)?',
515
+ default: true
516
+ }]);
517
+
518
+ if (quickInstall.all) {
519
+ selectedSkills = availableSkills;
520
+ selectedRules = availableRules;
521
+ selectedWorkflows = availableWorkflows;
522
+ options.installMemory = true; // Flag to install memory
523
+ } else {
524
+ // Manual selection...
525
+ // Seleccionar Skills
526
+ console.log(chalk.bold('\n🔹 Skills Disponibles:'));
527
+ const skillsAnswer = await inquirer.prompt([
528
+ {
529
+ type: 'checkbox',
530
+ name: 'skills',
531
+ message: 'Selecciona (Espacio para elegir, Enter para confirmar):',
532
+ choices: availableSkills.map(s => ({ name: s, checked: true })),
533
+ pageSize: 15
534
+ }
535
+ ]);
536
+ selectedSkills = skillsAnswer.skills;
537
+
538
+ // Seleccionar Rules
539
+ console.log(chalk.bold('\n🔹 Reglas Disponibles:'));
540
+ const rulesAnswer = await inquirer.prompt([
541
+ {
542
+ type: 'checkbox',
543
+ name: 'rules',
544
+ message: 'Selecciona (Espacio para elegir, Enter para confirmar):',
545
+ choices: availableRules.map(r => ({ name: r, checked: true })),
546
+ pageSize: 15
547
+ }
548
+ ]);
549
+ selectedRules = rulesAnswer.rules;
550
+
551
+ // Seleccionar Workflows
552
+ console.log(chalk.bold('\n🔹 Workflows Disponibles:'));
553
+ const workflowsAnswer = await inquirer.prompt([
554
+ {
555
+ type: 'checkbox',
556
+ name: 'workflows',
557
+ message: 'Selecciona (Espacio para elegir, Enter para confirmar):',
558
+ choices: availableWorkflows.map(w => ({ name: w, checked: true })),
559
+ pageSize: 15
560
+ }
561
+ ]);
562
+ selectedWorkflows = workflowsAnswer.workflows;
563
+
564
+ // Seleccionar Memory
565
+ console.log(chalk.bold('\n🔹 Memoria (Contexto):'));
566
+ const memoryAnswer = await inquirer.prompt([
567
+ {
568
+ type: 'confirm',
569
+ name: 'memory',
570
+ message: '¿Instalar estructura de Memoria (.agents/memory)?',
571
+ default: true
572
+ }
573
+ ]);
574
+ options.installMemory = memoryAnswer.memory;
575
+ }
576
+
577
+
578
+
579
+ console.log('');
580
+ const { confirm } = await inquirer.prompt([{
581
+ type: 'confirm',
582
+ name: 'confirm',
583
+ message: '¿Proceder con la instalación?',
584
+ default: true
585
+ }]);
586
+ if (!confirm) return;
587
+ }
588
+
589
+ // --- AGGRESSIVE ROOT CLEANUP ---
590
+ console.log(chalk.bold('\n🧹 Ejecutando limpieza de archivos root (Aislamiento de Agentes)...'));
591
+ // Definimos TODOS los posibles entry points
592
+ const allRootFiles = [
593
+ '.cursorrules', '.windsurfrules', '.windsurfrules.md', '.continuerules',
594
+ '.goosehints', '.roorules',
595
+ 'CLAUDE.md', 'GEMINI.md', 'openclaw.json'
596
+ ];
597
+
598
+ // Y determinamos cuáles SÍ deben existir según los agentes seleccionados
599
+ const requiredRootFiles = new Set();
600
+
601
+ // AGENTS.md es siempre intocable
602
+ requiredRootFiles.add('AGENTS.md');
603
+
604
+ for (const ide of targetIdes) {
605
+ if (ide.configFile && !ide.configFile.includes('/')) {
606
+ requiredRootFiles.add(ide.configFile);
607
+ }
608
+ if (ide.markerFile && !ide.markerFile.includes('/')) {
609
+ requiredRootFiles.add(ide.markerFile);
610
+ }
611
+ }
612
+
613
+ // Eliminamos todo lo que NO fue requerido
614
+ for (const file of allRootFiles) {
615
+ if (!requiredRootFiles.has(file)) {
616
+ const filePath = path.join(targetRoot, file);
617
+ if (fs.existsSync(filePath)) {
618
+ try {
619
+ fs.unlinkSync(filePath);
620
+ console.log(` ${chalk.green('✔')} Eliminado root config obsoleto o conflictivo: ${chalk.cyan(file)}`);
621
+ } catch (e) {
622
+ console.error(` ${chalk.red('❌')} Error al eliminar ${file}: ${e.message}`);
623
+ }
624
+ }
625
+ }
626
+ }
627
+
628
+ console.log('');
629
+ for (const ide of targetIdes) {
630
+ let currentInstallMethod = installMethod;
631
+ if (ide.forceCopy && currentInstallMethod === 'symlink') {
632
+ console.log(chalk.yellow(`⚠️ ${ide.name} detectado: Forzando método 'copy' (Mejor compatibilidad)`));
633
+ currentInstallMethod = 'copy';
634
+ }
635
+
636
+
637
+ if (selectedSkills.length > 0 && ide.skillsDir) {
638
+ const targetDir = path.join(targetRoot, ide.skillsDir);
639
+ console.log(chalk.bold(`\nInstalling Skills to ${chalk.cyan(targetDir)}:`));
640
+
641
+ try {
642
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
643
+
644
+ for (const skill of selectedSkills) {
645
+ const srcFolder = path.join(SOURCE_SKILLS, skill);
646
+ const destFolder = path.join(targetDir, skill);
647
+
648
+ if (fs.existsSync(srcFolder)) {
649
+ await applyFile(srcFolder, destFolder, currentInstallMethod);
650
+ console.log(` ${chalk.green('')} ${skill}/`);
651
+ }
652
+ }
653
+ } catch (e) {
654
+ console.error(chalk.red(`❌ Error installing skills for ${ide.name}: ${e.message}`));
655
+ }
656
+ }
657
+
658
+ // 4. Generate/Update Global Config File (Bootstrap)
659
+ let bootstrapStatus = 'SKIP';
660
+ if (ide.configFile) {
661
+ // Safety: Don't inject Markdown into JSON/YAML
662
+ if (ide.configFile.endsWith('.json') || ide.configFile.endsWith('.yaml') || ide.configFile.endsWith('.yml')) {
663
+ // console.log(chalk.gray(` ℹ Skipping bootstrap for ${ide.name} (Structured Config)`));
664
+ bootstrapStatus = 'SKIP';
665
+ } else {
666
+ const configPath = path.join(targetRoot, ide.configFile);
667
+ const relativeRulesPath = getRelLink(ide.configFile, '.agents/rules/00-master.md');
668
+ const relativeCatalogPath = getRelLink(ide.configFile, 'AGENTS.md');
669
+ const relativeContextPath = getRelLink(ide.configFile, 'CLAUDE.md');
670
+
671
+ // Usar template específico del agente si existe, si no usar contenido genérico
672
+ const AGENT_CONFIGS_TEMPLATE_DIR = path.join(__dirname, '.agents', 'templates', 'agent-configs');
673
+ const templateFile = ide.configTemplate
674
+ ? path.join(AGENT_CONFIGS_TEMPLATE_DIR, ide.configTemplate)
675
+ : path.join(AGENT_CONFIGS_TEMPLATE_DIR, '_generic.md');
676
+
677
+ let content;
678
+ if (fs.existsSync(templateFile) && !ide.configFile.endsWith('.json')) {
679
+ // Usar template del archivo, inyectando VERSION
680
+ content = fs.readFileSync(templateFile, 'utf8')
681
+ .replace(/\{\{VERSION\}\}/g, PKG_VERSION)
682
+ .replace(/\{\{MAJOR\}\}/g, PKG_VERSION.split('.')[0])
683
+ .replace(/\{\{SKILLS_DIR\}\}/g, ide.skillsDir || '.agents/skills')
684
+ .replace(/\{\{RULES_DIR\}\}/g, ide.rulesDir || '.agents/rules')
685
+ .replace(/\{\{WORKFLOWS_DIR\}\}/g, ide.workflowsDir || '.agents/workflows');
686
+ } else if (ide.configFile.endsWith('.json') && fs.existsSync(templateFile)) {
687
+ // JSON: usar template y reemplazar VERSION
688
+ content = fs.readFileSync(templateFile, 'utf8')
689
+ .replace(/\{\{VERSION\}\}/g, PKG_VERSION)
690
+ .replace(/\{\{MAJOR\}\}/g, PKG_VERSION.split('.')[0])
691
+ .replace(/\{\{SKILLS_DIR\}\}/g, ide.skillsDir || '.agents/skills')
692
+ .replace(/\{\{RULES_DIR\}\}/g, ide.rulesDir || '.agents/rules')
693
+ .replace(/\{\{WORKFLOWS_DIR\}\}/g, ide.workflowsDir || '.agents/workflows');
694
+ } else {
695
+ // Fallback: contenido genérico dinámico
696
+ content = `
697
+ # 🤖 LMAgent Framework v${PKG_VERSION}
698
+ > Contexto Activo: Este proyecto utiliza el estándar LMAgent V${PKG_VERSION.split('.')[0]}.
699
+
700
+ ## 🚨 SOURCE OF TRUTH (CEREBRO)
701
+ **TU CONTEXTO Y REGLAS VIVEN AQUÍ 👉 [AGENTS.md](${relativeCatalogPath})**
702
+ *Lee este archivo INMEDIATAMENTE para obtener tu identidad, skills y reglas operativas.*
703
+
704
+ ## 🔄 ARRANQUE AUTOMÁTICO (Haz esto al iniciar cada sesión)
705
+ 1. Lee [AGENTS.md](${relativeCatalogPath}) — Tu catálogo completo de capacidades
706
+ 2. Lee \`.agents/rules/00-master.md\` — Reglas y protocolo de trabajo
707
+ 3. Si existe \`.agents/memory/04-active-context.md\` — Recupera contexto previo
708
+ 4. Clasifica la tarea (Nivel 0-4) y activa el skill apropiado
709
+
710
+ ## 📁 RUTAS DE ENTORNO
711
+ - **Skills**: \`${ide.skillsDir || '.agents/skills'}\` (31 skills disponibles)
712
+ - **Rules**: \`${ide.rulesDir || '.agents/rules'}\` (11 reglas)
713
+ - **Workflows**: \`${ide.workflowsDir || '.agents/workflows'}\` (13 SOPs)
714
+ - **Memory**: \`.agents/memory/\`
715
+ - **Config**: \`.agents/config/\`
716
+
717
+ ## QUICK START TRIGGERS (Menu Rápido)
718
+ Usa estos comandos para activar un rol. Para el catálogo completo de 31 skills, consulta \`AGENTS.md\`.
719
+
720
+ | Trigger | Rol / Skill | Objetivo |
721
+ |:--- |:--- |:--- |
722
+ | \`/orch\` | **Orchestrator** | Clasificar y delegar. |
723
+ | \`/dev\` | **Backend** | APIs y Lógica. |
724
+ | \`/front\` | **Frontend** | UI/UX, React. |
725
+ | \`/pm\` | **Product** | PRDs y Roadmap. |
726
+ | \`/fix\` | **Debugger** | Análisis de bugs. |
727
+ | \`/arch\` | **Architect** | Diseño de sistemas. |
728
+
729
+ > **IMPORTANTE**: Para activar un skill, lee su \`SKILL.md\` completo en \`${ide.skillsDir || '.agents/skills'}/[nombre]/SKILL.md\`.
730
+
731
+ !! SYSTEM NOTE: You MUST read AGENTS.md at startup to understand the full framework. !!
732
+ `;
733
+ }
734
+ // If file exists, check if we need to append
735
+ try {
736
+ if (fs.existsSync(configPath)) {
737
+ // Check if it's a directory (Edge case: Cline legacy folders)
738
+ if (fs.statSync(configPath).isDirectory()) {
739
+ console.error(chalk.red(` ❌ Cannot bootstrap ${ide.configFile}: Is a directory.`));
740
+ bootstrapStatus = 'ERROR';
741
+ } else {
742
+ const existingContent = fs.readFileSync(configPath, 'utf8');
743
+ if (!existingContent.includes('QUICK START TRIGGERS')) {
744
+ fs.appendFileSync(configPath, '\n' + content);
745
+ bootstrapStatus = 'UPDATED';
746
+ } else {
747
+ bootstrapStatus = 'OK';
748
+ }
749
+ }
750
+ } else {
751
+ // Create parent dir if needed (for .github/copilot... etc)
752
+ if (!fs.existsSync(path.dirname(configPath))) fs.mkdirSync(path.dirname(configPath), { recursive: true });
753
+ fs.writeFileSync(configPath, content);
754
+ bootstrapStatus = 'CREATED';
755
+ }
756
+ } catch (e) {
757
+ console.error(chalk.red(` ❌ Error bootstrapping ${ide.name}: ${e.message}`));
758
+ bootstrapStatus = 'ERROR';
759
+ }
760
+ }
761
+ }
762
+
763
+ if (bootstrapStatus !== 'SKIP' && bootstrapStatus !== 'OK') {
764
+ console.log(` ${bootstrapStatus === 'CREATED' ? chalk.green('✔') : chalk.blue('')} ${ide.name} Bootstrap: ${bootstrapStatus}`);
765
+ }
766
+
767
+ // 4.1 Generate Bridge Rule
768
+ // Si el agente NO tiene configFile, necesita un archivo puente en rulesDir para auto-invocarse.
769
+ // Si tampoco tiene bridgeFile definido, usamos '00-lmagent.md' como default genérico.
770
+ const bridgeFile = ide.bridgeFile || (ide.rulesDir && !ide.configFile ? '00-lmagent.md' : null);
771
+ const needsBridge = bridgeFile && !ide.configFile;
772
+ // Garantizar que rulesDir existe siempre (para que el agente pueda detectar la instalación)
773
+ if (ide.rulesDir && !fs.existsSync(path.join(targetRoot, ide.rulesDir))) {
774
+ fs.mkdirSync(path.join(targetRoot, ide.rulesDir), { recursive: true });
775
+ }
776
+ if (ide.rulesDir && needsBridge) {
777
+ const bridgePath = path.join(targetRoot, ide.rulesDir, bridgeFile);
778
+ const relativeBridgeToRoot = path.join(ide.rulesDir, bridgeFile);
779
+ // Usar entry point universal (AGENTS.md) en vez de hardcodear CLAUDE.md
780
+ const agentEntryPoint = ide.configFile || 'AGENTS.md';
781
+ const relContext = getRelLink(relativeBridgeToRoot, agentEntryPoint);
782
+ const relCatalog = getRelLink(relativeBridgeToRoot, 'AGENTS.md');
783
+ const relRules = getRelLink(relativeBridgeToRoot, '.agents/rules/00-master.md');
784
+ const relMemory = getRelLink(relativeBridgeToRoot, '.agents/memory/04-active-context.md');
785
+
786
+ let bridgeContent = '';
787
+
788
+ if (bridgeFile.endsWith('.mdc')) {
789
+ // Cursor MDC Format
790
+ bridgeContent = `---
791
+ description: LMAgent Framework Entry Point - Use this rule to understand how to interact with the project skills and rules.
792
+ globs: **/*
793
+ ---
794
+
795
+ # 🤖 LMAgent Bridge Rule
796
+
797
+ Este proyecto está potenciado por **LMAgent v${PKG_VERSION}**.
798
+
799
+ ## 🚨 SOURCE OF TRUTH (CEREBRO)
800
+ **TU CONTEXTO Y REGLAS VIVEN AQUÍ 👉 [AGENTS.md](${relCatalog})**
801
+ *Lee este archivo INMEDIATAMENTE para obtener tu identidad, skills y reglas operativas.*
802
+
803
+ ## 🔄 ARRANQUE AUTOMÁTICO (Haz esto al iniciar)
804
+ 1. Lee [AGENTS.md](${relCatalog}) Tu catálogo completo de capacidades
805
+ 2. Lee [00-master.md](${relRules}) — Reglas y protocolo de trabajo
806
+ 3. Si existe [04-active-context.md](${relMemory}) Léelo para recuperar contexto de la sesión anterior
807
+ 4. Clasifica la tarea (Nivel 0-4) y activa el skill apropiado
808
+
809
+ ## 📁 RUTAS DE ENTORNO
810
+ - **Tus Skills**: \`${ide.skillsDir || '.agents/skills'}\` (31 skills disponibles)
811
+ - **Tus Rules**: \`${ide.rulesDir || '.agents/rules'}\` (11 reglas)
812
+ - **Tus Workflows**: \`${ide.workflowsDir || '.agents/workflows'}\` (13 SOPs)
813
+ - **Memory**: \`${ide.skillsDir ? ide.skillsDir.replace(/\/[^/]+$/, '/memory') : '.agents/memory'}\`
814
+ - **Config**: \`${ide.skillsDir ? ide.skillsDir.replace(/\/[^/]+$/, '/config') : '.agents/config'}\`
815
+
816
+ ## QUICK START TRIGGERS (Menu Rápido)
817
+ Usa estos comandos para activar un rol. Para el catálogo completo de 31 skills, consulta \`AGENTS.md\`.
818
+
819
+ | Trigger | Rol / Skill | Objetivo |
820
+ |:--- |:--- |:--- |
821
+ | \`/orch\` | **Orchestrator** | Clasificar y delegar. |
822
+ | \`/dev\` | **Backend** | APIs y Lógica. |
823
+ | \`/front\` | **Frontend** | UI/UX, React. |
824
+ | \`/pm\` | **Product** | PRDs y Roadmap. |
825
+ | \`/fix\` | **Debugger** | Análisis de bugs. |
826
+ | \`/arch\` | **Architect** | Diseño de sistemas. |
827
+
828
+ > **IMPORTANTE**: Para activar un skill, lee su \`SKILL.md\` completo en \`${ide.skillsDir || '.agents/skills'}/[nombre]/SKILL.md\`.
829
+
830
+ !! SYSTEM NOTE: You MUST read AGENTS.md at startup to understand the full framework. !!
831
+ `;
832
+ } else {
833
+ // Standard Markdown (Universal & Cline/Windsurf)
834
+ bridgeContent = `# 🤖 LMAgent Framework Entry Point
835
+
836
+ Este proyecto utiliza **LMAgent v${PKG_VERSION}**.
837
+
838
+ ## 🚨 SOURCE OF TRUTH (CEREBRO)
839
+ **TU CONTEXTO Y REGLAS VIVEN AQUÍ 👉 [AGENTS.md](${relCatalog})**
840
+ *Lee este archivo INMEDIATAMENTE para obtener tu identidad, skills y reglas operativas.*
841
+
842
+ ## 🔄 ARRANQUE AUTOMÁTICO (Haz esto al iniciar)
843
+ 1. Lee [AGENTS.md](${relCatalog}) — Tu catálogo completo de capacidades
844
+ 2. Lee [00-master.md](${relRules}) — Reglas y protocolo de trabajo
845
+ 3. Si existe [04-active-context.md](${relMemory}) — Léelo para recuperar contexto de la sesión anterior
846
+ 4. Clasifica la tarea (Nivel 0-4) y activa el skill apropiado
847
+
848
+ ## 📁 RUTAS DE ENTORNO
849
+ - **Tus Skills**: \`${ide.skillsDir || '.agents/skills'}\` (31 skills disponibles)
850
+ - **Tus Rules**: \`${ide.rulesDir || '.agents/rules'}\` (11 reglas)
851
+ - **Tus Workflows**: \`${ide.workflowsDir || '.agents/workflows'}\` (13 SOPs)
852
+ - **Memory**: \`${ide.skillsDir ? ide.skillsDir.replace(/\/[^/]+$/, '/memory') : '.agents/memory'}\`
853
+ - **Config**: \`${ide.skillsDir ? ide.skillsDir.replace(/\/[^/]+$/, '/config') : '.agents/config'}\`
854
+
855
+ ## ⚡ QUICK START TRIGGERS (Menu Rápido)
856
+ Usa estos comandos para activar un rol. Para el catálogo completo de 31 skills, consulta \`AGENTS.md\`.
857
+
858
+ | Trigger | Rol / Skill | Objetivo |
859
+ |:--- |:--- |:--- |
860
+ | \`/orch\` | **Orchestrator** | Clasificar y delegar. |
861
+ | \`/dev\` | **Backend** | APIs y Lógica. |
862
+ | \`/front\` | **Frontend** | UI/UX, React. |
863
+ | \`/pm\` | **Product** | PRDs y Roadmap. |
864
+ | \`/fix\` | **Debugger** | Análisis de bugs. |
865
+ | \`/arch\` | **Architect** | Diseño de sistemas. |
866
+
867
+ > **IMPORTANTE**: Para activar un skill, lee su \`SKILL.md\` completo en \`${ide.skillsDir || '.agents/skills'}/[nombre]/SKILL.md\`.
868
+ `;
869
+ }
870
+
871
+ // CLEANUP: Legacy Cursor Skills Directory
872
+ // Since we moved skills to .cursor/rules/skills, we must remove .cursor/skills to avoid duplicates
873
+ if (ide.value === 'cursor') {
874
+ const legacySkillsDir = path.join(targetRoot, '.cursor/skills');
875
+ if (fs.existsSync(legacySkillsDir)) {
876
+ try {
877
+ fs.rmSync(legacySkillsDir, { recursive: true, force: true });
878
+ console.log(` ${chalk.yellow('🗑 Eliminado directorio obsoleto:')} .cursor / skills(Movido a.cursor / rules / skills)`);
879
+ } catch (e) {
880
+ console.error(chalk.red(` ⚠️ No se pudo eliminar.cursor / skills: ${e.message} `));
881
+ }
882
+ }
883
+ }
884
+
885
+ try {
886
+ if (!fs.existsSync(path.dirname(bridgePath))) fs.mkdirSync(path.dirname(bridgePath), { recursive: true });
887
+ fs.writeFileSync(bridgePath, bridgeContent);
888
+ console.log(` ${chalk.green('✔')} ${ide.name} Bridge Rule: ${bridgeFile} `);
889
+ } catch (e) {
890
+ console.error(chalk.red(` ❌ Error creating bridge for ${ide.name}: ${e.message} `));
891
+ }
892
+ }
893
+ // 2. Install RULES (Files)
894
+ if (selectedRules.length > 0 && ide.rulesDir) {
895
+ const targetDir = path.join(targetRoot, ide.rulesDir);
896
+ console.log(chalk.bold(`\nInstalling Rules to ${chalk.cyan(targetDir)}: `));
897
+
898
+ try {
899
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
900
+
901
+ // CLEANUP: Remove legacy rules (V2 & Duplicates)
902
+ const legacyRules = [
903
+ '_bootstrap.md', '_bootstrap.mdc', '00-bootstrap.md',
904
+ 'agents-ia.md', 'stack.md', 'testing.md', 'security.md', 'code-style.md', 'documentation.md',
905
+ 'workflow.md', 'api-design.md', 'automations-n8n.md', 'frontend.md', 'backend.md'
906
+ ];
907
+ for (const legacy of legacyRules) {
908
+ const legacyPath = path.join(targetDir, legacy);
909
+ if (fs.existsSync(legacyPath)) {
910
+ fs.unlinkSync(legacyPath);
911
+ console.log(` ${chalk.yellow('🗑 Eliminado regla obsoleta:')} ${legacy} `);
912
+ }
913
+ }
914
+
915
+ for (const rule of selectedRules) {
916
+ const srcVal = path.join(SOURCE_RULES, rule);
917
+ const destVal = path.join(targetDir, rule);
918
+
919
+ if (fs.existsSync(srcVal)) {
920
+ await applyFile(srcVal, destVal, currentInstallMethod);
921
+ console.log(` ${chalk.blue('✔')} ${rule} `);
922
+ }
923
+ }
924
+ } catch (e) {
925
+ console.error(chalk.red(`❌ Error installing rules for ${ide.name}: ${e.message} `));
926
+ }
927
+ }
928
+
929
+ // 3. Install WORKFLOWS (Files)
930
+ if (selectedWorkflows.length > 0 && ide.workflowsDir) {
931
+ const targetDir = path.join(targetRoot, ide.workflowsDir);
932
+ console.log(chalk.bold(`\nInstalling Workflows to ${chalk.cyan(targetDir)}: `));
933
+
934
+ try {
935
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
936
+
937
+ for (const wf of selectedWorkflows) {
938
+ const srcVal = path.join(SOURCE_WORKFLOWS, wf);
939
+ const destVal = path.join(targetDir, wf);
940
+
941
+ if (fs.existsSync(srcVal)) {
942
+ await applyFile(srcVal, destVal, currentInstallMethod);
943
+ console.log(` ${chalk.magenta('')} ${wf} `);
944
+ }
945
+ }
946
+ } catch (e) {
947
+ console.error(chalk.red(`❌ Error installing workflows for ${ide.name}: ${e.message} `));
948
+ }
949
+ }
950
+
951
+
952
+
953
+ if (SOURCE_MEMORY && ide.skillsDir) {
954
+ const parentDir = path.dirname(ide.skillsDir);
955
+ const targetDir = path.join(targetRoot, parentDir, 'memory');
956
+ const targetDirLower = targetDir.toLowerCase();
957
+ const sourceMemoryLower = SOURCE_MEMORY.toLowerCase();
958
+
959
+ if (targetDirLower !== sourceMemoryLower) {
960
+ try {
961
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
962
+ copyRecursiveSync(SOURCE_MEMORY, targetDir, true);
963
+ console.log(` ${chalk.cyan('✔')} Memory(Context) optimized.`);
964
+ } catch (e) {
965
+ console.error(chalk.red(`❌ Error installing memory for ${ide.name}: ${e.message} `));
966
+ }
967
+ } else {
968
+ console.log(` ${chalk.cyan('ℹ')} Memory(Context) already in origin path.`);
969
+ }
970
+ }
971
+ }
972
+
973
+ // 🔄 Sincronizar Catálogo de Skills en AGENTS.md y 00-master.md
974
+ await syncSkillCatalog(targetRoot);
975
+
976
+ console.log(gradient.pastel.multiline('\n✨ Instalación Finalizada ✨'));
977
+
978
+ console.log(chalk.gray('================================================================'));
979
+ console.log(chalk.bold.green('🎉 ¡Todo listo! Aquí tienes cómo usar tus nuevos superpoderes:'));
980
+ console.log('');
981
+
982
+ // Mensaje dinámico según agentes instalados
983
+ const ideNames = targetIdes.map(i => i.name).join(', ');
984
+ console.log(chalk.cyan(`🤖 Agentes configurados: ${chalk.bold(ideNames)} `));
985
+ console.log('');
986
+ console.log(chalk.white(' 1. Abre tu agente en este proyecto — leerá el contexto automáticamente.'));
987
+ console.log(chalk.white(' 2. Usa los triggers para activar un rol específico:'));
988
+ console.log(chalk.gray(' /dev Backend | /front → Frontend | /arch → Arquitecto'));
989
+ console.log(chalk.gray(' /fix → Debugger | /pm → Product | /orch → Orchestrator'));
990
+ console.log('');
991
+ console.log(chalk.dim(' 💡 Ejecuta `lmagent doctor` para verificar la instalación.'));
992
+ console.log(chalk.dim(' 💡 Ejecuta `lmagent tokens` para ver el consumo de tokens del framework.'));
993
+ console.log(chalk.gray('================================================================'));
994
+ }
995
+
996
+ // ─── Sincronización Dinámica del Catálogo de Skills ───────────────────────────
997
+ // Escanea .agents/skills/*/SKILL.md, extrae frontmatter y regenera las tablas
998
+ // entre <!-- SKILLS_CATALOG_START --> y <!-- SKILLS_CATALOG_END --> en:
999
+ // - AGENTS.md (tabla de skills con trigger, nombre y directorio)
1000
+ // - .agents/rules/00-master.md (tabla de skills con trigger y descripción)
1001
+ async function syncSkillCatalog(projectRoot) {
1002
+ const skillsDir = path.join(projectRoot, '.agents', 'skills');
1003
+ if (!fs.existsSync(skillsDir)) return;
1004
+
1005
+ // 1. Escanear todos los SKILL.md y extraer frontmatter
1006
+ const skills = [];
1007
+ const skillDirs = fs.readdirSync(skillsDir).filter(d => {
1008
+ const p = path.join(skillsDir, d);
1009
+ return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'SKILL.md'));
1010
+ });
1011
+
1012
+ for (const dir of skillDirs) {
1013
+ const skillMdPath = path.join(skillsDir, dir, 'SKILL.md');
1014
+ try {
1015
+ const content = fs.readFileSync(skillMdPath, 'utf8');
1016
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
1017
+ if (!fmMatch) continue;
1018
+
1019
+ const fm = fmMatch[1];
1020
+ const name = (fm.match(/^name:\s*(.+)$/m) || [])[1]?.trim() || dir;
1021
+ const description = (fm.match(/^description:\s*(.+)$/m) || [])[1]?.trim() || '';
1022
+
1023
+ // Extract triggers array from YAML
1024
+ const triggersMatch = fm.match(/triggers:\s*\n((?:\s+-\s*.+\n?)+)/);
1025
+ let triggers = [];
1026
+ if (triggersMatch) {
1027
+ triggers = triggersMatch[1].match(/\s+-\s*(.+)/g)?.map(t => t.replace(/^\s+-\s*/, '').trim().replace(/["']/g, '')) || [];
1028
+ }
1029
+
1030
+ skills.push({ slug: dir, name, description, triggers });
1031
+ } catch (e) {
1032
+ // Silently skip malformed skills
1033
+ }
1034
+ }
1035
+
1036
+ if (skills.length === 0) return;
1037
+
1038
+ console.log(chalk.bold(`\n📚 Sincronizando Catálogo de Skills (${skills.length} skills detectados)...`));
1039
+
1040
+ // 2. Generar tabla para AGENTS.md (formato: Trigger | Skill | Directorio)
1041
+ const agentsTableLines = skills.map(s => {
1042
+ const trigger = s.triggers.length > 0 ? `\`${s.triggers[0]}\`` : `\`/${s.slug.split('-')[0]}\``;
1043
+ return `| ${trigger} | **${s.slug}** | \`.agents/skills/${s.slug}/\` |`;
1044
+ });
1045
+
1046
+ const agentsTableContent = `| Trigger | Skill | Directorio |
1047
+ |:---|:---|:---|
1048
+ ${agentsTableLines.join('\n')}`;
1049
+
1050
+ // 3. Generar tabla para 00-master.md (formato: Skill | Triggers | Descripción)
1051
+ const masterTableLines = skills.map(s => {
1052
+ const triggerStr = s.triggers.length > 0
1053
+ ? s.triggers.map(t => `\`${t}\``).join(', ')
1054
+ : `\`/${s.slug.split('-')[0]}\``;
1055
+ return `| **${s.slug}** | ${triggerStr} | ${s.description} |`;
1056
+ });
1057
+
1058
+ const masterTableContent = `| Skill | Triggers | Descripción |
1059
+ |-------|----------|-------------|
1060
+ ${masterTableLines.join('\n')}`;
1061
+
1062
+ // 4. Inyectar en AGENTS.md
1063
+ const agentsPath = path.join(projectRoot, 'AGENTS.md');
1064
+ if (fs.existsSync(agentsPath)) {
1065
+ let agentsContent = fs.readFileSync(agentsPath, 'utf8');
1066
+ const agentsRegex = /<!-- SKILLS_CATALOG_START -->[\s\S]*?<!-- SKILLS_CATALOG_END -->/;
1067
+ if (agentsRegex.test(agentsContent)) {
1068
+ const newSection = `<!-- SKILLS_CATALOG_START -->\n${agentsTableContent}\n<!-- SKILLS_CATALOG_END -->`;
1069
+ agentsContent = agentsContent.replace(agentsRegex, newSection);
1070
+ fs.writeFileSync(agentsPath, agentsContent, 'utf8');
1071
+ console.log(` ${chalk.green('✔')} AGENTS.md — Catálogo actualizado (${skills.length} skills)`);
1072
+ }
1073
+ }
1074
+
1075
+ // 5. Inyectar en 00-master.md (buscar en project's .agents/rules/)
1076
+ const masterPath = path.join(projectRoot, '.agents', 'rules', '00-master.md');
1077
+ if (fs.existsSync(masterPath)) {
1078
+ let masterContent = fs.readFileSync(masterPath, 'utf8');
1079
+ const masterRegex = /<!-- SKILLS_CATALOG_START -->[\s\S]*?<!-- SKILLS_CATALOG_END -->/;
1080
+ if (masterRegex.test(masterContent)) {
1081
+ const newSection = `<!-- SKILLS_CATALOG_START -->\n${masterTableContent}\n<!-- SKILLS_CATALOG_END -->`;
1082
+ masterContent = masterContent.replace(masterRegex, newSection);
1083
+ fs.writeFileSync(masterPath, masterContent, 'utf8');
1084
+ console.log(` ${chalk.green('✔')} 00-master.md — Catálogo actualizado (${skills.length} skills)`);
1085
+ }
1086
+ }
1087
+ }
1088
+
1089
+ async function applyFile(source, dest, method) {
1090
+ const srcPath = path.resolve(source);
1091
+ const destPath = path.resolve(dest);
1092
+
1093
+ // Case-insensitive check for Windows compatibility
1094
+ if (srcPath.toLowerCase() === destPath.toLowerCase()) {
1095
+ // console.log(chalk.gray(` (Skipping self - install: ${ path.basename(source) })`));
1096
+ return;
1097
+ }
1098
+
1099
+ try {
1100
+ if (fs.existsSync(dest) || (fs.existsSync(path.dirname(dest)) && fs.readdirSync(path.dirname(dest)).includes(path.basename(dest)))) {
1101
+ const lstat = fs.lstatSync(dest);
1102
+ if (lstat.isSymbolicLink()) {
1103
+ fs.unlinkSync(dest); // Safe to remove old symlink
1104
+ } else if (lstat.isDirectory()) {
1105
+ // AUGMENTATION: No borramos el directorio real para no perder archivos del usuario.
1106
+ // Si el usuario pidió symlink pero hay un directorio real, forzamos merge por copia.
1107
+ if (method === 'symlink') method = 'copy';
1108
+ } else {
1109
+ fs.unlinkSync(dest); // It is a file, overwrite it
1110
+ }
1111
+ }
1112
+ } catch (e) { }
1113
+
1114
+ const destDir = path.dirname(dest);
1115
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
1116
+
1117
+ const srcStat = fs.statSync(source);
1118
+ const isDir = srcStat.isDirectory();
1119
+
1120
+ if (method === 'symlink') {
1121
+ try {
1122
+ const type = isDir ? 'junction' : 'file';
1123
+ fs.symlinkSync(source, dest, type);
1124
+ } catch (e) {
1125
+ try {
1126
+ if (isDir) {
1127
+ copyRecursiveSync(source, dest, true);
1128
+ } else {
1129
+ fs.copyFileSync(source, dest);
1130
+ }
1131
+ const isWindows = os.platform() === 'win32';
1132
+ const msg = isWindows && !isDir
1133
+ ? `(Symlink falló[Requiere Admin / DevMode en Win].Copiado.)`
1134
+ : `(Symlink falló, se usó copia)`;
1135
+ console.log(chalk.yellow(` ${msg} `));
1136
+ } catch (err) {
1137
+ console.error(chalk.red(` Error copiando ${path.basename(dest)}: ${err.message} `));
1138
+ }
1139
+ }
1140
+ } else {
1141
+ if (isDir) {
1142
+ copyRecursiveSync(source, dest, true);
1143
+ } else {
1144
+ fs.copyFileSync(source, dest);
1145
+ }
1146
+ }
1147
+ }
1148
+
1149
+ function copyRecursiveSync(src, dest, overwrite) {
1150
+ if (fs.cpSync) {
1151
+ try {
1152
+ fs.cpSync(src, dest, { recursive: true, force: overwrite, errorOnExist: false });
1153
+ } catch (e) {
1154
+ console.error(chalk.red(`Error copying(cpSync) ${path.basename(src)}: ${e.message} `));
1155
+ // Fallback manual implementation just in case
1156
+ if (fs.existsSync(src)) {
1157
+ const stat = fs.statSync(src);
1158
+ if (stat.isDirectory()) {
1159
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
1160
+ fs.readdirSync(src).forEach(child => {
1161
+ copyRecursiveSync(path.join(src, child), path.join(dest, child), overwrite);
1162
+ });
1163
+ } else {
1164
+ fs.copyFileSync(src, dest);
1165
+ }
1166
+ }
1167
+ }
1168
+ } else {
1169
+ // Fallback for older Node versions
1170
+ const exists = fs.existsSync(src);
1171
+ const stats = exists && fs.statSync(src);
1172
+ const isDirectory = exists && stats.isDirectory();
1173
+ if (isDirectory) {
1174
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
1175
+ fs.readdirSync(src).forEach(function (childItemName) {
1176
+ copyRecursiveSync(path.join(src, childItemName),
1177
+ path.join(dest, childItemName), overwrite);
1178
+ });
1179
+ } else {
1180
+ if (overwrite || !fs.existsSync(dest)) {
1181
+ fs.copyFileSync(src, dest);
1182
+ }
1183
+ }
1184
+ }
1185
+ }
1186
+
1187
+ function getAllItems(dir, isNested) {
1188
+ if (!fs.existsSync(dir)) return [];
1189
+ const items = fs.readdirSync(dir);
1190
+ if (isNested) {
1191
+ return items.filter(item => {
1192
+ const p = path.join(dir, item);
1193
+ return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'SKILL.md'));
1194
+ });
1195
+ } else {
1196
+ return items.filter(item => item.endsWith('.md') || item.endsWith('.txt') || item.endsWith('.cursorrules') || item.endsWith('.toml'));
1197
+ }
1198
+ }
1199
+
1200
+ // ============================================
1201
+ // INIT: Inicializar proyecto con LMAgent
1202
+ // ============================================
1203
+
1204
+ async function runInit(options) {
1205
+ let targetIdes = []; // Initialize targetIdes
1206
+ console.clear();
1207
+ const branding = figlet.textSync('LMAGENT', { font: 'ANSI Shadow' });
1208
+ console.log(gradient.pastel.multiline(branding));
1209
+ console.log(gradient.cristal(' by QuBit\n'));
1210
+
1211
+ const projectRoot = process.cwd();
1212
+ const targetRoot = projectRoot; // Fix for ReferenceError in runInit
1213
+ console.log(chalk.cyan(`📦 Inicializando proyecto LMAgent en: ${chalk.bold(projectRoot)} \n`));
1214
+
1215
+ // Verificar si ya está inicializado
1216
+ const agentsExists = fs.existsSync(path.join(projectRoot, 'AGENTS.md'));
1217
+ if (agentsExists && !options.force) {
1218
+ console.log(chalk.yellow('⚠️ Este proyecto ya tiene AGENTS.md'));
1219
+ if (!options.yes) {
1220
+ const { overwrite } = await inquirer.prompt([{
1221
+ type: 'confirm',
1222
+ name: 'overwrite',
1223
+ message: '¿Sobrescribir archivos existentes?',
1224
+ default: false
1225
+ }]);
1226
+ if (!overwrite) {
1227
+ console.log(chalk.yellow('Cancelado. Usa --force para forzar.'));
1228
+ return;
1229
+ }
1230
+ }
1231
+ }
1232
+
1233
+ let filesToCopy = [...INIT_FILES];
1234
+ let dirsToCopy = [...INIT_DIRS];
1235
+
1236
+ // Modo interactivo: preguntar qué copiar
1237
+ if (!options.yes) {
1238
+ const answers = await inquirer.prompt([
1239
+ {
1240
+ type: 'checkbox',
1241
+ name: 'files',
1242
+ message: 'Archivos de entry point a copiar:',
1243
+ choices: INIT_FILES.map(f => ({
1244
+ name: `${f.src} - ${f.desc} `,
1245
+ value: f.src,
1246
+ checked: true
1247
+ }))
1248
+ },
1249
+ {
1250
+ type: 'checkbox',
1251
+ name: 'dirs',
1252
+ message: 'Directorios a copiar:',
1253
+ choices: INIT_DIRS.map(d => ({
1254
+ name: `${d.src}/ - ${d.desc}`,
1255
+ value: d.src,
1256
+ checked: true
1257
+ }))
1258
+ }
1259
+ ]);
1260
+ dirsToCopy = INIT_DIRS.filter(d => answers.dirs.includes(d.src));
1261
+
1262
+ // Seleccionar IDE para destino de archivos
1263
+ console.log('');
1264
+ const ideAnswer = await inquirer.prompt([
1265
+ {
1266
+ type: 'checkbox',
1267
+ name: 'ides',
1268
+ message: 'Selecciona tu IDE principal (para ubicar las carpetas):',
1269
+ choices: IDE_CONFIGS.filter(i => i.value !== 'custom').map(i => ({
1270
+ name: i.name,
1271
+ value: i.value,
1272
+ checked: i.value === 'cursor'
1273
+ }))
1274
+ }
1275
+ ]);
1276
+ targetIdes = IDE_CONFIGS.filter(i => ideAnswer.ides.includes(i.value));
1277
+ } else {
1278
+ // Defaults for non-interactive
1279
+ targetIdes = [IDE_CONFIGS.find(i => i.value === 'cursor')];
1280
+ }
1281
+
1282
+ // Copiar archivos del framework a la carpeta del Agente (Clean Root)
1283
+ console.log(chalk.bold('\n📦 Instalando framework en directorios de Agente:'));
1284
+
1285
+ for (const ide of targetIdes) {
1286
+ if (!ide.skillsDir) continue; // Skip custom/manual if no dir defined
1287
+
1288
+ // Determinar "Agent Root" (ej: .cursor/ o .github/)
1289
+ // Asume que skillsDir es "root/skills", así que dirname obtiene "root"
1290
+ const agentRootDir = path.join(targetRoot, path.dirname(ide.skillsDir));
1291
+
1292
+ console.log(chalk.dim(` Destino: ${agentRootDir}`));
1293
+
1294
+ // Crear directorio root si no existe
1295
+ if (!fs.existsSync(agentRootDir)) fs.mkdirSync(agentRootDir, { recursive: true });
1296
+
1297
+ // 5. Install Root Configs (CLAUDE.md, AGENTS.md) with Prompt
1298
+ console.log(chalk.bold('\nChecking Root Configurations:'));
1299
+ for (const file of INIT_FILES) {
1300
+ const srcPath = path.join(__dirname, file.src);
1301
+ const destPath = path.join(targetRoot, file.src);
1302
+
1303
+ if (fs.existsSync(srcPath)) {
1304
+ if (!fs.existsSync(destPath)) {
1305
+ let content = fs.readFileSync(srcPath, 'utf8');
1306
+ if (file.versionTemplate) content = content.replace(/\{\{VERSION\}\}/g, PKG_VERSION);
1307
+ fs.writeFileSync(destPath, content, 'utf8');
1308
+ console.log(` ${chalk.green('✔')} ${file.src} (Created)`);
1309
+ } else {
1310
+ // Exists: Ask to overwrite (unless force/yes)
1311
+ let shouldOverwrite = false;
1312
+ if (options.force) {
1313
+ shouldOverwrite = true;
1314
+ } else if (!options.yes) {
1315
+ const answer = await inquirer.prompt([{
1316
+ type: 'confirm',
1317
+ name: 'overwrite',
1318
+ message: `⚠️ ${file.src} ya existe. ¿Sobrescribir?`,
1319
+ default: false
1320
+ }]);
1321
+ shouldOverwrite = answer.overwrite;
1322
+ }
1323
+
1324
+ if (shouldOverwrite) {
1325
+ let content = fs.readFileSync(srcPath, 'utf8');
1326
+ if (file.versionTemplate) content = content.replace(/\{\{VERSION\}\}/g, PKG_VERSION);
1327
+ fs.writeFileSync(destPath, content, 'utf8');
1328
+ console.log(` ${chalk.yellow('✎')} ${file.src} (Overwritten)`);
1329
+ } else {
1330
+ console.log(` ${chalk.gray('SKIP')} ${file.src} (Kept existing)`);
1331
+ }
1332
+ }
1333
+ }
1334
+ }
1335
+
1336
+ // Copiar Directorios (docs, config, templates)
1337
+ for (const dir of dirsToCopy) {
1338
+ const src = path.join(__dirname, dir.src);
1339
+ const dest = path.join(agentRootDir, dir.src);
1340
+ if (fs.existsSync(src)) {
1341
+ copyRecursiveSync(src, dest, true); // Force overwrite
1342
+ console.log(` ${chalk.green('')} ${dir.src}/ -> ${path.dirname(ide.skillsDir)}/${dir.src}/`);
1343
+
1344
+ // CLEANUP: If docs, remove assets (legacy logo, etc.)
1345
+ if (dir.src === 'docs') {
1346
+ const assetsDir = path.join(dest, 'assets');
1347
+ if (fs.existsSync(assetsDir)) {
1348
+ try {
1349
+ fs.rmSync(assetsDir, { recursive: true, force: true });
1350
+ console.log(` ${chalk.yellow('🗑 Eliminado assets heredados (logo, etc.)')}`);
1351
+ } catch (e) { }
1352
+ }
1353
+ }
1354
+ }
1355
+ }
1356
+ }
1357
+
1358
+ // Crear .env.example si no existe
1359
+ const envExampleDest = path.join(projectRoot, '.env.example');
1360
+ if (!fs.existsSync(envExampleDest)) {
1361
+ const envContent = `# ============================================
1362
+ # LMAgent Project - Environment Variables
1363
+ # ============================================
1364
+ # Copiar este archivo a .env y completar los valores
1365
+
1366
+ # Database
1367
+ DATABASE_URL=postgresql://user:password@localhost:5432/dbname
1368
+
1369
+ # Redis
1370
+ REDIS_URL=redis://localhost:6379
1371
+
1372
+ # Security
1373
+ JWT_SECRET=change-this-to-a-strong-secret-minimum-32-characters
1374
+
1375
+ # LLM API Keys
1376
+ OPENAI_API_KEY=sk-...
1377
+ ANTHROPIC_API_KEY=sk-ant-...
1378
+ GOOGLE_API_KEY=...
1379
+
1380
+ # n8n (si aplica)
1381
+ N8N_WEBHOOK_URL=https://n8n.yourserver.com/webhook
1382
+
1383
+ # Environment
1384
+ ENVIRONMENT=development
1385
+ DEBUG=true
1386
+ `;
1387
+ fs.writeFileSync(envExampleDest, envContent);
1388
+ console.log(` ${chalk.green('✔')} .env.example ${chalk.green('(nuevo)')}`);
1389
+ } else {
1390
+ console.log(` ${chalk.blue('')} .env.example ya existe, no se sobrescribe`);
1391
+ }
1392
+
1393
+ // Resumen
1394
+ console.log(gradient.pastel.multiline(`\n✨ Proyecto inicializado con LMAgent v${PKG_VERSION} ✨`));
1395
+ console.log('');
1396
+ console.log(chalk.cyan('Próximos pasos:'));
1397
+ console.log(` 1. ${chalk.bold('lmagent install')} - Instalar skills/rules/workflows en tu IDE`);
1398
+ console.log(` 2. Editar ${chalk.bold('.env.example')} → ${chalk.bold('.env')} con tus credenciales`);
1399
+ console.log(` 3. Leer ${chalk.bold('AGENTS.md')} para conocer las capacidades disponibles`);
1400
+ console.log('');
1401
+
1402
+ // Preguntar si quiere ejecutar install también
1403
+ if (!options.yes) {
1404
+ const { runInstallNow } = await inquirer.prompt([{
1405
+ type: 'confirm',
1406
+ name: 'runInstallNow',
1407
+ message: '¿Ejecutar lmagent install ahora para conectar al IDE?',
1408
+ default: true
1409
+ }]);
1410
+ if (runInstallNow) {
1411
+ console.log('');
1412
+ await runInstall(options);
1413
+ }
1414
+ } else {
1415
+ console.log(chalk.cyan('💡 Ejecuta `lmagent install` para conectar al IDE.\n'));
1416
+ }
1417
+ }
1418
+
1419
+ // ============================================
1420
+ // DOCTOR: Verificar configuración del proyecto
1421
+ // ============================================
1422
+
1423
+ async function runDoctor() {
1424
+ console.clear();
1425
+ const branding = figlet.textSync('LMAGENT', { font: 'ANSI Shadow' });
1426
+ console.log(gradient.pastel.multiline(branding));
1427
+ console.log(gradient.cristal(' Doctor - Diagnóstico\n'));
1428
+
1429
+ const projectRoot = process.cwd();
1430
+ let issues = 0;
1431
+ let ok = 0;
1432
+
1433
+ console.log(chalk.bold('🔍 Verificando proyecto en: ' + chalk.cyan(projectRoot) + '\n'));
1434
+
1435
+ // 1. Archivos de entry point
1436
+ console.log(chalk.bold('📄 Entry Points:'));
1437
+ for (const file of INIT_FILES) {
1438
+ const exists = fs.existsSync(path.join(projectRoot, file.src));
1439
+ if (exists) {
1440
+ console.log(` ${chalk.green('✔')} ${file.src}`);
1441
+ ok++;
1442
+ } else {
1443
+ console.log(` ${chalk.red('✘')} ${file.src} - ${chalk.red('FALTANTE')} → ejecuta ${chalk.bold('lmagent init')}`);
1444
+ issues++;
1445
+ }
1446
+ }
1447
+
1448
+ // 2. Directorios de configuración
1449
+ console.log(chalk.bold('\n📁 Configuración:'));
1450
+ for (const dir of INIT_DIRS) {
1451
+ const exists = fs.existsSync(path.join(projectRoot, dir.src));
1452
+ if (exists) {
1453
+ console.log(` ${chalk.green('✔')} ${dir.src}/`);
1454
+ ok++;
1455
+ } else {
1456
+ console.log(` ${chalk.yellow('⚠')} ${dir.src}/ - Opcional, ejecuta ${chalk.bold('lmagent init')} para copiar`);
1457
+ }
1458
+ }
1459
+
1460
+ // 3. Detectar IDEs configurados
1461
+ console.log(chalk.bold('\n🔧 IDEs detectados:'));
1462
+ let ideFound = false;
1463
+ for (const ide of IDE_CONFIGS) {
1464
+ if (ide.value === 'custom') continue;
1465
+ const rulesExist = ide.rulesDir && fs.existsSync(path.join(projectRoot, ide.rulesDir));
1466
+ const skillsExist = ide.skillsDir && fs.existsSync(path.join(projectRoot, ide.skillsDir));
1467
+ const markerExist = ide.markerFile && fs.existsSync(path.join(projectRoot, ide.markerFile));
1468
+
1469
+ if (rulesExist || skillsExist || markerExist) {
1470
+ ideFound = true;
1471
+ const parts = [];
1472
+ if (rulesExist) parts.push('rules');
1473
+ if (skillsExist) parts.push('skills');
1474
+ console.log(` ${chalk.green('✔')} ${ide.name} (${parts.join(', ')})`);
1475
+ ok++;
1476
+
1477
+ // Verificar que tiene los skills correctos
1478
+ if (skillsExist) {
1479
+ const installedSkills = fs.readdirSync(path.join(projectRoot, ide.skillsDir))
1480
+ .filter(item => fs.statSync(path.join(projectRoot, ide.skillsDir, item)).isDirectory());
1481
+
1482
+ // Calcular skills esperados dinámicamente
1483
+ const expectedSkillsCount = getAllItems(PACKAGE_SKILLS_DIR, true).length;
1484
+ const skillCount = installedSkills.length;
1485
+
1486
+ if (skillCount < expectedSkillsCount) {
1487
+ console.log(` ${chalk.yellow('⚠')} Solo ${skillCount}/${expectedSkillsCount} skills instalados → ejecuta ${chalk.bold('lmagent install')}`);
1488
+ } else {
1489
+ console.log(` ${chalk.green('✔')} ${skillCount} skills instalados`);
1490
+ }
1491
+ }
1492
+ }
1493
+ }
1494
+ if (!ideFound) {
1495
+ console.log(` ${chalk.red('✘')} Ningún IDE detectado → ejecuta ${chalk.bold('lmagent install')}`);
1496
+ issues++;
1497
+ }
1498
+
1499
+ // 4. Verificar .env
1500
+ console.log(chalk.bold('\n🔒 Seguridad:'));
1501
+ const envExists = fs.existsSync(path.join(projectRoot, '.env'));
1502
+ const envExampleExists = fs.existsSync(path.join(projectRoot, '.env.example'));
1503
+ const gitignoreExists = fs.existsSync(path.join(projectRoot, '.gitignore'));
1504
+
1505
+ if (envExampleExists) {
1506
+ console.log(` ${chalk.green('✔')} .env.example existe`);
1507
+ ok++;
1508
+ } else {
1509
+ console.log(` ${chalk.yellow('⚠')} .env.example no encontrado`);
1510
+ }
1511
+
1512
+ if (envExists) {
1513
+ console.log(` ${chalk.green('✔')} .env existe`);
1514
+ ok++;
1515
+ } else {
1516
+ console.log(` ${chalk.yellow('⚠')} .env no encontrado (necesario para ejecutar)`);
1517
+ }
1518
+
1519
+ if (gitignoreExists) {
1520
+ const gitignore = fs.readFileSync(path.join(projectRoot, '.gitignore'), 'utf-8');
1521
+ if (gitignore.includes('.env')) {
1522
+ console.log(` ${chalk.green('✔')} .env está en .gitignore`);
1523
+ ok++;
1524
+ } else {
1525
+ console.log(` ${chalk.red('✘')} .env NO está en .gitignore → ${chalk.red('RIESGO DE SEGURIDAD')}`);
1526
+ issues++;
1527
+ }
1528
+ }
1529
+
1530
+ // Resumen
1531
+ console.log('');
1532
+ if (issues === 0) {
1533
+ console.log(gradient.pastel(`\n✨ Todo en orden! ${ok} verificaciones pasadas.\n`));
1534
+ } else {
1535
+ console.log(chalk.yellow(`\n⚠️ ${issues} problema(s) encontrado(s), ${ok} verificaciones OK.\n`));
1536
+ }
1537
+ }
1538
+
1539
+ // Helper: Calculate relative Markdown link
1540
+ function getRelLink(fromRelPath, toRelPath) {
1541
+ const fromDir = path.dirname(fromRelPath);
1542
+ let rel = path.relative(fromDir, toRelPath);
1543
+ if (!rel.startsWith('.')) rel = './' + rel;
1544
+ return rel.replace(/\\/g, '/'); // Force forward slashes for Markdown
1545
+ }
1546
+
1547
+ // Helper: Contar archivos recursivamente
1548
+ function getAllItemsFlat(dir) {
1549
+ let results = [];
1550
+ if (!fs.existsSync(dir)) return results;
1551
+ const items = fs.readdirSync(dir);
1552
+ for (const item of items) {
1553
+ const fullPath = path.join(dir, item);
1554
+ if (fs.statSync(fullPath).isDirectory()) {
1555
+ results = results.concat(getAllItemsFlat(fullPath));
1556
+ } else {
1557
+ results.push(fullPath);
1558
+ }
1559
+ }
1560
+ return results;
1561
+ }
1562
+
1563
+
1564
+ // Execute CLI only if run directly
1565
+ if (require.main === module) {
1566
+ if (process.argv.length === 2) {
1567
+ runInstall({});
1568
+ } else {
1569
+ program.parse(process.argv);
1570
+ }
1571
+ }
1572
+
1573
+ module.exports = { IDE_CONFIGS };