@luquimbo/bi-superpowers 1.1.3 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/build-plugin.js +52 -0
- package/bin/cli.js +6 -4
- package/bin/commands/install.js +248 -150
- package/bin/commands/install.test.js +212 -0
- package/bin/lib/agents.js +43 -0
- package/bin/lib/generators/claude-plugin.js +10 -2
- package/bin/lib/licensing/storage.js +41 -1
- package/bin/lib/skills.js +26 -8
- package/bin/utils/projects.js +3 -2
- package/package.json +1 -1
- package/skills/contributions/SKILL.md +1 -1
- package/skills/data-model-design/SKILL.md +1 -1
- package/skills/data-modeling/SKILL.md +1 -1
- package/skills/data-quality/SKILL.md +1 -1
- package/skills/dax/SKILL.md +1 -1
- package/skills/dax-doctor/SKILL.md +1 -1
- package/skills/dax-udf/SKILL.md +1 -1
- package/skills/deployment/SKILL.md +1 -1
- package/skills/excel-formulas/SKILL.md +1 -1
- package/skills/fabric-scripts/SKILL.md +1 -1
- package/skills/fast-standard/SKILL.md +1 -1
- package/skills/governance/SKILL.md +1 -1
- package/skills/migration-assistant/SKILL.md +1 -1
- package/skills/model-documenter/SKILL.md +1 -1
- package/skills/pbi-connect/SKILL.md +1 -1
- package/skills/power-query/SKILL.md +1 -1
- package/skills/project-kickoff/SKILL.md +1 -1
- package/skills/query-performance/SKILL.md +1 -1
- package/skills/report-design/SKILL.md +1 -1
- package/skills/report-layout/SKILL.md +1 -1
- package/skills/rls-design/SKILL.md +1 -1
- package/skills/semantic-model/SKILL.md +1 -1
- package/skills/testing-validation/SKILL.md +1 -1
- package/skills/theme-tweaker/SKILL.md +1 -1
package/bin/build-plugin.js
CHANGED
|
@@ -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
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
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,
|
package/bin/commands/install.js
CHANGED
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
* Install Command - Multi-agent skill installer
|
|
3
3
|
* ===============================================
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
39
|
-
* @param {string} baseDir -
|
|
40
|
-
* @returns {string[]}
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
78
|
-
* @returns {Promise<string[]>}
|
|
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('
|
|
87
|
-
console.log('
|
|
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
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* @param {string}
|
|
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
|
-
*
|
|
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
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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 (
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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('
|
|
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(`
|
|
185
|
-
console.log(chalk.gray(` Skills: ${skillDirs.length}
|
|
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
|
-
|
|
208
|
-
|
|
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
|
|
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(
|
|
348
|
+
chalk.cyan(
|
|
349
|
+
`\n Instalando ${skillDirs.length} skills para ${selectedAgents.length} agentes...\n`
|
|
350
|
+
)
|
|
227
351
|
);
|
|
228
352
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
let
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
//
|
|
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}/ (${
|
|
370
|
+
console.log(chalk.green(` ✓ ${UNIVERSAL_DIR}/ (${skillDirs.length} skills)${universalSuffix}`));
|
|
246
371
|
|
|
247
|
-
//
|
|
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
|
-
//
|
|
299
|
-
|
|
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(`
|
|
393
|
+
chalk.green.bold(`Instalados ${skillDirs.length} skills para ${totalAgents} agentes`) +
|
|
303
394
|
'\n\n' +
|
|
304
|
-
chalk.gray('
|
|
395
|
+
chalk.gray('Los skills están listos. Abrí tu agente AI y empezá a usarlos.') +
|
|
305
396
|
'\n' +
|
|
306
|
-
chalk.gray('
|
|
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;
|