@luquimbo/bi-superpowers 1.1.2 → 1.2.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bi-superpowers",
3
3
  "description": "Claude Code plugin for Power BI, Microsoft Fabric, and semantic model workflows powered by the official Microsoft MCP servers.",
4
- "version": "1.1.2",
4
+ "version": "1.2.0",
5
5
  "author": {
6
6
  "name": "Lucas Sanchez"
7
7
  }
@@ -2,13 +2,57 @@
2
2
 
3
3
  /**
4
4
  * Build the repository-root Claude Code plugin artifacts.
5
+ *
6
+ * Genera los archivos del plugin desde las fuentes en src/content/skills/
7
+ * y valida que los outputs críticos hayan sido creados correctamente.
8
+ * Este script corre en el prepack de npm publish.
5
9
  */
6
10
 
11
+ const fs = require('fs');
7
12
  const path = require('path');
8
13
  const pluginGenerator = require('./lib/generators/claude-plugin');
9
14
  const { loadSkills } = require('./lib/skills');
10
15
  const pkg = require('../package.json');
11
16
 
17
+ /**
18
+ * Verifica que todos los archivos críticos del plugin hayan sido generados.
19
+ * @param {string} packageDir - Directorio raíz del paquete
20
+ * @throws {Error} Si falta cualquier archivo crítico
21
+ */
22
+ function verifyPluginOutputs(packageDir) {
23
+ const criticalFiles = ['.claude-plugin/plugin.json', '.mcp.json', 'commands', 'skills'];
24
+
25
+ const missing = [];
26
+ for (const relPath of criticalFiles) {
27
+ const fullPath = path.join(packageDir, relPath);
28
+ if (!fs.existsSync(fullPath)) {
29
+ missing.push(relPath);
30
+ }
31
+ }
32
+
33
+ if (missing.length > 0) {
34
+ throw new Error(
35
+ `Plugin generation completed but outputs are missing: ${missing.join(', ')}. ` +
36
+ 'Check src/content/skills/ and the generator logic.'
37
+ );
38
+ }
39
+
40
+ // Verify the skills/ directory has at least one SKILL.md (sanity check)
41
+ const skillsDir = path.join(packageDir, 'skills');
42
+ const skillSubdirs = fs
43
+ .readdirSync(skillsDir, { withFileTypes: true })
44
+ .filter((d) => d.isDirectory());
45
+ const skillsWithMd = skillSubdirs.filter((d) =>
46
+ fs.existsSync(path.join(skillsDir, d.name, 'SKILL.md'))
47
+ );
48
+
49
+ if (skillsWithMd.length === 0) {
50
+ throw new Error(
51
+ `Plugin generation completed but no SKILL.md files were created in ${skillsDir}/`
52
+ );
53
+ }
54
+ }
55
+
12
56
  async function main() {
13
57
  const packageDir = path.dirname(__dirname);
14
58
  const skills = loadSkills({
@@ -16,12 +60,20 @@ async function main() {
16
60
  preferLocal: true,
17
61
  });
18
62
 
63
+ if (skills.length === 0) {
64
+ throw new Error(
65
+ `No skills found in ${packageDir}/src/content/skills/. Cannot build plugin without sources.`
66
+ );
67
+ }
68
+
19
69
  await pluginGenerator.generate(packageDir, skills, {
20
70
  packageDir,
21
71
  version: pkg.version,
22
72
  usePluginRootLauncher: true,
23
73
  libraryPrefix: 'library',
24
74
  });
75
+
76
+ verifyPluginOutputs(packageDir);
25
77
  }
26
78
 
27
79
  main().catch((error) => {
package/bin/cli.js CHANGED
@@ -127,10 +127,12 @@ const AI_TOOLS = generators ? generators.AI_TOOLS : {};
127
127
  * - Developer: Advanced tools for content management (xray, checkup, scan, sentinel, powers)
128
128
  * - Legacy: Old command names maintained for backward compatibility
129
129
  */
130
- // Commands are registered lazily to avoid TDZ issues with const wrappers.
131
- // Core commands use hoisted function declarations; developer commands use
132
- // createCommandWrapper which is defined later. We populate the map in
133
- // registerCommands() which runs before main().
130
+ // Commands are registered in two phases to avoid TDZ (temporal dead zone) errors.
131
+ // Phase 1: hoisted function declarations (showHelp, initProject, etc.) go directly
132
+ // into the object literal below safe because `function` declarations hoist.
133
+ // Phase 2: wrapper-based commands that depend on `createCommandWrapper` (defined
134
+ // further down in the file) are attached imperatively after that function exists.
135
+ // See the `commands.xray = runSearch;` block after the wrapper `const`s.
134
136
  const commands = {
135
137
  // Core commands - basic info and status (hoisted functions, safe here)
136
138
  help: showHelp,
@@ -2,12 +2,19 @@
2
2
  * Install Command - Multi-agent skill installer
3
3
  * ===============================================
4
4
  *
5
- * Installs BI Agent Superpowers skills into the correct directories
6
- * for each AI coding agent. Inspired by the `npx skills` CLI from Vercel Labs.
5
+ * Instala los skills de BI Agent Superpowers en los directorios
6
+ * correctos para cada agente AI. Inspirado en el CLI `npx skills` de
7
+ * Vercel Labs.
7
8
  *
8
- * Skills are always installed at the user level (~/) to protect licensed content.
9
+ * Los skills siempre se instalan a nivel de usuario (~/) para proteger
10
+ * contenido licenciado. Un usuario podría copiarlos manualmente, pero
11
+ * no se commitean al repo del proyecto por accidente.
9
12
  *
10
- * Usage:
13
+ * Nota: este comando NO requiere licencia. Los skills son gratis de
14
+ * instalar; la licencia controla acceso a contenido premium vía
15
+ * `super unlock` + `super kickoff`.
16
+ *
17
+ * Uso:
11
18
  * npx @luquimbo/bi-superpowers install
12
19
  * super install
13
20
  * super install --agent claude-code --agent codex
@@ -20,24 +27,12 @@ const fs = require('fs');
20
27
  const path = require('path');
21
28
  const os = require('os');
22
29
  const readline = require('readline');
23
-
24
- // Agent registry: each agent's skill directory path (relative to home directory)
25
- // Order matches the interactive installer display order
26
- const AGENTS = {
27
- 'github-copilot': { name: 'GitHub Copilot', dir: '.github/skills' },
28
- 'claude-code': { name: 'Claude Code', dir: '.claude/skills' },
29
- codex: { name: 'Codex (OpenAI)', dir: '.agents/skills' },
30
- 'gemini-cli': { name: 'Gemini CLI', dir: '.gemini/skills' },
31
- kilo: { name: 'Kilo Code', dir: '.kilocode/skills' },
32
- };
33
-
34
- // Universal path — most agents read from .agents/skills/
35
- const UNIVERSAL_DIR = '.agents/skills';
30
+ const { AGENTS, UNIVERSAL_DIR } = require('../lib/agents');
36
31
 
37
32
  /**
38
- * Detect which agents are available by checking for their config directories
39
- * @param {string} baseDir - Directory to check (project root or home)
40
- * @returns {string[]} Array of detected agent IDs
33
+ * Detecta qué agentes están instalados revisando sus directorios de config.
34
+ * @param {string} baseDir - Directorio base (home del usuario)
35
+ * @returns {string[]} IDs de agentes detectados
41
36
  */
42
37
  function detectAgents(baseDir) {
43
38
  const detected = [];
@@ -52,7 +47,7 @@ function detectAgents(baseDir) {
52
47
  }
53
48
 
54
49
  /**
55
- * Create readline interface for interactive prompts
50
+ * Crea una interface de readline para prompts interactivos.
56
51
  */
57
52
  function createReadline() {
58
53
  return readline.createInterface({
@@ -62,7 +57,7 @@ function createReadline() {
62
57
  }
63
58
 
64
59
  /**
65
- * Prompt user with a question
60
+ * Envuelve una pregunta de readline en una Promise.
66
61
  */
67
62
  function prompt(rl, question) {
68
63
  return new Promise((resolve) => {
@@ -71,11 +66,11 @@ function prompt(rl, question) {
71
66
  }
72
67
 
73
68
  /**
74
- * Display a numbered list and let user pick multiple items
69
+ * Muestra una lista numerada y deja al usuario elegir múltiples items.
75
70
  * @param {readline.Interface} rl
76
71
  * @param {Array<{id: string, name: string}>} items
77
- * @param {string[]} preselected - IDs to preselect
78
- * @returns {Promise<string[]>} Selected IDs
72
+ * @param {string[]} preselected - IDs preseleccionados
73
+ * @returns {Promise<string[]>} IDs seleccionados
79
74
  */
80
75
  async function selectMultiple(rl, items, preselected = []) {
81
76
  items.forEach((item, i) => {
@@ -83,8 +78,8 @@ async function selectMultiple(rl, items, preselected = []) {
83
78
  console.log(` ${i + 1}) ${marker} ${item.name}`);
84
79
  });
85
80
  console.log();
86
- console.log(' Enter numbers separated by commas (e.g. 1,2,3)');
87
- console.log(' Press Enter for detected agents, or "a" for all');
81
+ console.log(' Ingresa números separados por comas (ej: 1,2,3)');
82
+ console.log(' Presiona Enter para los detectados, o "a" para todos');
88
83
 
89
84
  const answer = await prompt(rl, '\n > ');
90
85
 
@@ -105,9 +100,10 @@ async function selectMultiple(rl, items, preselected = []) {
105
100
  }
106
101
 
107
102
  /**
108
- * Copy a skill directory to a target location
109
- * @param {string} srcDir - Source skill directory (containing SKILL.md)
110
- * @param {string} destDir - Destination directory
103
+ * Copia un directorio de skill recursivamente.
104
+ * Los errores de fs se propagan al caller para manejo centralizado.
105
+ * @param {string} srcDir - Directorio de skill fuente (contiene SKILL.md)
106
+ * @param {string} destDir - Directorio destino
111
107
  */
112
108
  function copySkillDir(srcDir, destDir) {
113
109
  if (!fs.existsSync(destDir)) {
@@ -128,43 +124,199 @@ function copySkillDir(srcDir, destDir) {
128
124
  }
129
125
 
130
126
  /**
131
- * Main install command handler
132
- * @param {string[]} args - CLI arguments
133
- * @param {Object} config - Command config from CLI
127
+ * Formatea un error de filesystem con hint útil según el código.
134
128
  */
135
- async function installCommand(args, config) {
136
- const chalk = require('chalk');
137
- const boxen = require('boxen');
129
+ function formatFsError(err, context) {
130
+ const codeHints = {
131
+ EACCES: 'Permiso denegado. Revisa los permisos del directorio.',
132
+ EPERM: 'Operación no permitida. En Windows, probá ejecutar como Administrador.',
133
+ ENOSPC: 'No hay espacio en disco.',
134
+ ENOENT: 'Archivo o directorio no existe.',
135
+ EROFS: 'Sistema de archivos en solo lectura.',
136
+ };
137
+ const hint = codeHints[err.code] || '';
138
+ return `${context}: ${err.message}${hint ? `\n ${hint}` : ''}`;
139
+ }
138
140
 
139
- const isYes = args.includes('--yes') || args.includes('-y');
140
- const isAll = args.includes('--all');
141
- const agentFlags = [];
141
+ /**
142
+ * Parsea las flags de CLI y devuelve un objeto de opciones.
143
+ * Valida que cada --agent/-a tenga un valor asociado.
144
+ */
145
+ function parseArgs(args) {
146
+ const opts = {
147
+ isYes: args.includes('--yes') || args.includes('-y'),
148
+ isAll: args.includes('--all'),
149
+ agentFlags: [],
150
+ };
142
151
 
143
- // Parse --agent flags
144
152
  for (let i = 0; i < args.length; i++) {
145
- if ((args[i] === '--agent' || args[i] === '-a') && args[i + 1]) {
146
- agentFlags.push(args[i + 1]);
153
+ if (args[i] === '--agent' || args[i] === '-a') {
154
+ const next = args[i + 1];
155
+ if (next === undefined || next.startsWith('-')) {
156
+ // Falta valor para --agent; avisamos y seguimos sin ese flag
157
+ console.warn(`⚠ Flag ${args[i]} sin valor. Uso: ${args[i]} <agente-id>. Ignorando.`);
158
+ continue;
159
+ }
160
+ opts.agentFlags.push(next);
147
161
  i++;
148
162
  }
149
163
  }
150
164
 
151
- // Always install at user level (home directory) to protect licensed content
165
+ return opts;
166
+ }
167
+
168
+ /**
169
+ * Resuelve qué agentes instalar según las flags y el modo interactivo.
170
+ * @returns {Promise<string[]>} IDs de agentes seleccionados
171
+ */
172
+ async function resolveSelectedAgents(opts, baseDir, chalk) {
173
+ if (opts.isAll) {
174
+ return Object.keys(AGENTS);
175
+ }
176
+
177
+ if (opts.agentFlags.length > 0) {
178
+ const known = opts.agentFlags.filter((a) => AGENTS[a]);
179
+ const unknown = opts.agentFlags.filter((a) => !AGENTS[a]);
180
+ if (unknown.length > 0) {
181
+ console.log(chalk.yellow(` Agentes desconocidos: ${unknown.join(', ')}`));
182
+ console.log(chalk.gray(` Disponibles: ${Object.keys(AGENTS).join(', ')}\n`));
183
+ }
184
+ return known;
185
+ }
186
+
187
+ if (opts.isYes) {
188
+ return Object.keys(AGENTS);
189
+ }
190
+
191
+ // Modo interactivo
192
+ const detected = detectAgents(baseDir);
193
+ console.log(chalk.cyan(' Seleccioná los agentes donde querés instalar:\n'));
194
+
195
+ const items = Object.entries(AGENTS).map(([id, agent]) => ({
196
+ id,
197
+ name: detected.includes(id) ? `${agent.name} ${chalk.green('(detectado)')}` : agent.name,
198
+ }));
199
+
200
+ const rl = createReadline();
201
+ try {
202
+ return await selectMultiple(rl, items, detected);
203
+ } finally {
204
+ rl.close();
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Instala los skills en los directorios seleccionados.
210
+ * @returns {{agentResults: Array, copyFallbacks: number}}
211
+ */
212
+ function performInstall(skillsSourceDir, skillDirs, selectedAgents, baseDir) {
213
+ // Siempre instalamos primero en el path universal
214
+ const universalTarget = path.join(baseDir, UNIVERSAL_DIR);
215
+ for (const skill of skillDirs) {
216
+ const src = path.join(skillsSourceDir, skill);
217
+ const dest = path.join(universalTarget, skill);
218
+ copySkillDir(src, dest);
219
+ }
220
+
221
+ // Symlinks o copias para los directorios específicos de agentes
222
+ const agentResults = [];
223
+ let copyFallbacks = 0;
224
+
225
+ for (const agentId of selectedAgents) {
226
+ const agent = AGENTS[agentId];
227
+ if (agent.dir === UNIVERSAL_DIR) continue; // Ya manejado por el universal
228
+
229
+ const agentTarget = path.join(baseDir, agent.dir);
230
+
231
+ // Si ya existe y no es symlink, copiamos ahí directamente
232
+ if (fs.existsSync(agentTarget) && !fs.lstatSync(agentTarget).isSymbolicLink()) {
233
+ for (const skill of skillDirs) {
234
+ copySkillDir(path.join(skillsSourceDir, skill), path.join(agentTarget, skill));
235
+ }
236
+ agentResults.push({ agent: agent.name, method: 'copied', dir: agent.dir });
237
+ continue;
238
+ }
239
+
240
+ const parentDir = path.dirname(agentTarget);
241
+ if (!fs.existsSync(parentDir)) {
242
+ fs.mkdirSync(parentDir, { recursive: true });
243
+ }
244
+
245
+ // Eliminar symlink existente si hay uno (para poder recrearlo)
246
+ if (fs.existsSync(agentTarget) || fs.lstatSync(agentTarget, { throwIfNoEntry: false })) {
247
+ try {
248
+ fs.unlinkSync(agentTarget);
249
+ } catch (_) {
250
+ /* no existe, seguimos */
251
+ }
252
+ }
253
+
254
+ try {
255
+ // Symlink relativo del dir del agente al dir universal
256
+ const relPath = path.relative(parentDir, universalTarget);
257
+ fs.symlinkSync(relPath, agentTarget);
258
+ agentResults.push({ agent: agent.name, method: 'symlinked', dir: agent.dir });
259
+ } catch (symlinkErr) {
260
+ // Fallback a copia si el symlink falla (ej: Windows sin permisos)
261
+ copyFallbacks++;
262
+ if (!fs.existsSync(agentTarget)) {
263
+ fs.mkdirSync(agentTarget, { recursive: true });
264
+ }
265
+ for (const skill of skillDirs) {
266
+ copySkillDir(path.join(skillsSourceDir, skill), path.join(agentTarget, skill));
267
+ }
268
+ agentResults.push({
269
+ agent: agent.name,
270
+ method: 'copied',
271
+ dir: agent.dir,
272
+ fallbackReason: symlinkErr.code || symlinkErr.message,
273
+ });
274
+ }
275
+ }
276
+
277
+ return { agentResults, copyFallbacks };
278
+ }
279
+
280
+ /**
281
+ * Handler principal del comando install.
282
+ * @param {string[]} args - Argumentos CLI
283
+ * @param {Object} config - Config del comando desde el CLI
284
+ */
285
+ async function installCommand(args, config) {
286
+ const chalk = require('chalk');
287
+ const boxen = require('boxen');
288
+
289
+ const opts = parseArgs(args);
290
+
291
+ // Siempre a nivel usuario (home) para proteger contenido licenciado
152
292
  const baseDir = os.homedir();
153
293
 
154
- // Find skills from the package
155
- const packageDir = config.packageDir || path.dirname(__dirname);
294
+ // Localizar los skills del paquete
295
+ const packageDir = config.packageDir || path.dirname(path.dirname(__dirname));
156
296
  const skillsSourceDir = path.join(packageDir, 'skills');
157
297
 
158
298
  if (!fs.existsSync(skillsSourceDir)) {
159
- console.error(chalk.red('Skills directory not found. Try reinstalling the package.'));
299
+ console.error(
300
+ chalk.red(
301
+ 'Directorio de skills no encontrado. Reinstalá el paquete con: npm install -g @luquimbo/bi-superpowers'
302
+ )
303
+ );
160
304
  process.exit(1);
161
305
  }
162
306
 
163
- // Read available skills
164
- const skillDirs = fs
165
- .readdirSync(skillsSourceDir, { withFileTypes: true })
166
- .filter((d) => d.isDirectory() && fs.existsSync(path.join(skillsSourceDir, d.name, 'SKILL.md')))
167
- .map((d) => d.name);
307
+ // Leer skills disponibles
308
+ let skillDirs;
309
+ try {
310
+ skillDirs = fs
311
+ .readdirSync(skillsSourceDir, { withFileTypes: true })
312
+ .filter(
313
+ (d) => d.isDirectory() && fs.existsSync(path.join(skillsSourceDir, d.name, 'SKILL.md'))
314
+ )
315
+ .map((d) => d.name);
316
+ } catch (err) {
317
+ console.error(chalk.red(formatFsError(err, 'No pude leer los skills')));
318
+ process.exit(1);
319
+ }
168
320
 
169
321
  // Header
170
322
  console.log(
@@ -172,7 +324,7 @@ async function installCommand(args, config) {
172
324
  chalk.bold.cyan('BI Agent Superpowers') +
173
325
  chalk.gray(` v${config.version}`) +
174
326
  '\n' +
175
- chalk.gray('Multi-agent skill installer'),
327
+ chalk.gray('Instalador multi-agente'),
176
328
  {
177
329
  padding: 1,
178
330
  borderStyle: 'round',
@@ -181,129 +333,68 @@ async function installCommand(args, config) {
181
333
  )
182
334
  );
183
335
 
184
- console.log(chalk.gray(` Install path: ~/${UNIVERSAL_DIR}/`));
185
- console.log(chalk.gray(` Skills: ${skillDirs.length} available\n`));
186
-
187
- // Determine which agents to install for
188
- let selectedAgents;
189
-
190
- if (isAll) {
191
- selectedAgents = Object.keys(AGENTS);
192
- } else if (agentFlags.length > 0) {
193
- selectedAgents = agentFlags.filter((a) => AGENTS[a]);
194
- const unknown = agentFlags.filter((a) => !AGENTS[a]);
195
- if (unknown.length > 0) {
196
- console.log(chalk.yellow(` Unknown agents: ${unknown.join(', ')}`));
197
- console.log(chalk.gray(` Available: ${Object.keys(AGENTS).join(', ')}\n`));
198
- }
199
- } else if (isYes) {
200
- selectedAgents = Object.keys(AGENTS);
201
- } else {
202
- // Interactive mode: detect and ask
203
- const detected = detectAgents(baseDir);
204
-
205
- console.log(chalk.cyan(' Select agents to install for:\n'));
336
+ console.log(chalk.gray(` Ruta de instalación: ~/${UNIVERSAL_DIR}/`));
337
+ console.log(chalk.gray(` Skills: ${skillDirs.length} disponibles\n`));
206
338
 
207
- const items = Object.entries(AGENTS).map(([id, agent]) => ({
208
- id,
209
- name: detected.includes(id) ? `${agent.name} ${chalk.green('(detected)')}` : agent.name,
210
- }));
211
-
212
- const rl = createReadline();
213
- try {
214
- selectedAgents = await selectMultiple(rl, items, detected);
215
- } finally {
216
- rl.close();
217
- }
218
- }
339
+ // Resolver qué agentes instalar
340
+ const selectedAgents = await resolveSelectedAgents(opts, baseDir, chalk);
219
341
 
220
342
  if (selectedAgents.length === 0) {
221
- console.log(chalk.yellow('\n No agents selected. Nothing to install.'));
343
+ console.log(chalk.yellow('\n Ningún agente seleccionado. Nada que instalar.'));
222
344
  return;
223
345
  }
224
346
 
225
347
  console.log(
226
- chalk.cyan(`\n Installing ${skillDirs.length} skills for ${selectedAgents.length} agents...\n`)
348
+ chalk.cyan(
349
+ `\n Instalando ${skillDirs.length} skills para ${selectedAgents.length} agentes...\n`
350
+ )
227
351
  );
228
352
 
229
- // Always install to universal path first
230
- const universalTarget = path.join(baseDir, UNIVERSAL_DIR);
231
- let installedCount = 0;
232
-
233
- for (const skill of skillDirs) {
234
- const src = path.join(skillsSourceDir, skill);
235
- const dest = path.join(universalTarget, skill);
236
- copySkillDir(src, dest);
237
- installedCount++;
353
+ // Instalar
354
+ let agentResults;
355
+ let copyFallbacks;
356
+ try {
357
+ const result = performInstall(skillsSourceDir, skillDirs, selectedAgents, baseDir);
358
+ agentResults = result.agentResults;
359
+ copyFallbacks = result.copyFallbacks;
360
+ } catch (err) {
361
+ console.error(chalk.red(formatFsError(err, 'Falló la instalación')));
362
+ process.exit(1);
238
363
  }
239
364
 
240
- // List agents whose default dir is the universal path (e.g. Codex)
365
+ // Universal
241
366
  const universalAgents = selectedAgents
242
367
  .filter((id) => AGENTS[id] && AGENTS[id].dir === UNIVERSAL_DIR)
243
368
  .map((id) => AGENTS[id].name);
244
369
  const universalSuffix = universalAgents.length > 0 ? ` — ${universalAgents.join(', ')}` : '';
245
- console.log(chalk.green(` ✓ ${UNIVERSAL_DIR}/ (${installedCount} skills)${universalSuffix}`));
370
+ console.log(chalk.green(` ✓ ${UNIVERSAL_DIR}/ (${skillDirs.length} skills)${universalSuffix}`));
246
371
 
247
- // Symlink agent-specific directories to universal path
248
- const agentResults = [];
249
- for (const agentId of selectedAgents) {
250
- const agent = AGENTS[agentId];
251
- if (agent.dir === UNIVERSAL_DIR) continue; // Already handled
252
-
253
- const agentTarget = path.join(baseDir, agent.dir);
254
-
255
- // If the directory already exists and is not a symlink, copy instead
256
- if (fs.existsSync(agentTarget) && !fs.lstatSync(agentTarget).isSymbolicLink()) {
257
- // Copy skills into existing directory
258
- for (const skill of skillDirs) {
259
- copySkillDir(path.join(skillsSourceDir, skill), path.join(agentTarget, skill));
260
- }
261
- agentResults.push({ agent: agent.name, method: 'copied', dir: agent.dir });
262
- } else {
263
- // Create symlink to universal directory
264
- const parentDir = path.dirname(agentTarget);
265
- if (!fs.existsSync(parentDir)) {
266
- fs.mkdirSync(parentDir, { recursive: true });
267
- }
268
-
269
- // Remove existing symlink if present
270
- if (fs.existsSync(agentTarget)) {
271
- fs.unlinkSync(agentTarget);
272
- }
273
-
274
- try {
275
- // Relative symlink from agent dir to universal dir
276
- const relPath = path.relative(parentDir, universalTarget);
277
- fs.symlinkSync(relPath, agentTarget);
278
- agentResults.push({ agent: agent.name, method: 'symlinked', dir: agent.dir });
279
- } catch (_e) {
280
- // Fallback to copy if symlink fails (e.g. Windows without admin)
281
- if (!fs.existsSync(agentTarget)) {
282
- fs.mkdirSync(agentTarget, { recursive: true });
283
- }
284
- for (const skill of skillDirs) {
285
- copySkillDir(path.join(skillsSourceDir, skill), path.join(agentTarget, skill));
286
- }
287
- agentResults.push({ agent: agent.name, method: 'copied', dir: agent.dir });
288
- }
289
- }
290
- }
291
-
292
- // Print results
372
+ // Por agente
293
373
  for (const result of agentResults) {
294
374
  const icon = result.method === 'symlinked' ? '→' : '✓';
295
375
  console.log(chalk.green(` ${icon} ${result.dir}/ (${result.method}) — ${result.agent}`));
296
376
  }
297
377
 
298
- // Summary
299
- const totalAgents = agentResults.length + 1; // +1 for universal
378
+ // Aviso si hubo fallbacks de symlink a copia
379
+ if (copyFallbacks > 0) {
380
+ console.log(
381
+ chalk.yellow(
382
+ `\n ⚠ ${copyFallbacks} agente(s) usaron copia en vez de symlink ` +
383
+ '(probable Windows sin permisos de admin).\n' +
384
+ " Re-ejecutá 'super install' tras cada upgrade para refrescar los archivos."
385
+ )
386
+ );
387
+ }
388
+
389
+ // Resumen
390
+ const totalAgents = agentResults.length + (universalAgents.length > 0 ? 1 : 0);
300
391
  console.log(
301
392
  boxen(
302
- chalk.green.bold(`Installed ${installedCount} skills for ${totalAgents} agents`) +
393
+ chalk.green.bold(`Instalados ${skillDirs.length} skills para ${totalAgents} agentes`) +
303
394
  '\n\n' +
304
- chalk.gray('Skills are ready to use. Open your AI agent and start prompting.') +
395
+ chalk.gray('Los skills están listos. Abrí tu agente AI y empezá a usarlos.') +
305
396
  '\n' +
306
- chalk.gray('Example: "Help me write a DAX measure for YTD sales"'),
397
+ chalk.gray('Ejemplo: "Ayudame a escribir una medida DAX de ventas YTD"'),
307
398
  {
308
399
  padding: 1,
309
400
  margin: { top: 1 },
@@ -314,4 +405,11 @@ async function installCommand(args, config) {
314
405
  );
315
406
  }
316
407
 
408
+ // Exports internos para testing
317
409
  module.exports = installCommand;
410
+ module.exports.parseArgs = parseArgs;
411
+ module.exports.detectAgents = detectAgents;
412
+ module.exports.copySkillDir = copySkillDir;
413
+ module.exports.formatFsError = formatFsError;
414
+ module.exports.AGENTS = AGENTS;
415
+ module.exports.UNIVERSAL_DIR = UNIVERSAL_DIR;