@luquimbo/bi-superpowers 4.1.6 → 5.0.1
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/marketplace.json +8 -6
- package/.claude-plugin/plugin.json +1 -1
- package/.claude-plugin/skill-manifest.json +35 -19
- package/.plugin/plugin.json +1 -1
- package/AGENTS.md +150 -26
- package/CHANGELOG.md +489 -14
- package/README.md +103 -114
- package/bin/cli.js +7 -1
- package/bin/commands/diff.js +2 -2
- package/bin/commands/install.js +58 -45
- package/bin/commands/lint.js +2 -2
- package/bin/commands/validate-projects.js +425 -0
- package/bin/lib/generators/claude-plugin.js +31 -7
- package/bin/lib/generators/shared.js +11 -7
- package/bin/lib/mcp-config.js +22 -2
- package/bin/lib/skills.js +8 -8
- package/bin/mcp/powerbi-modeling-launcher.js +8 -4
- package/bin/postinstall.js +14 -12
- package/bin/utils/mcp-detect.js +11 -11
- package/commands/bi-connect.md +418 -0
- package/commands/bi-dax.md +385 -0
- package/commands/{project-kickoff.md → bi-kickoff.md} +78 -47
- package/commands/bi-modeling.md +395 -0
- package/commands/bi-performance.md +455 -0
- package/commands/bi-start.md +39 -27
- package/desktop-extension/manifest.json +2 -2
- package/package.json +3 -2
- package/skills/bi-connect/SKILL.md +420 -0
- package/skills/{pbi-connect → bi-connect}/scripts/update-check.js +1 -1
- package/skills/bi-dax/SKILL.md +387 -0
- package/skills/{report-design → bi-dax}/scripts/update-check.js +1 -1
- package/skills/{project-kickoff → bi-kickoff}/SKILL.md +79 -48
- package/skills/{project-kickoff → bi-kickoff}/scripts/update-check.js +1 -1
- package/skills/bi-modeling/SKILL.md +397 -0
- package/skills/bi-modeling/scripts/update-check.js +403 -0
- package/skills/bi-performance/SKILL.md +457 -0
- package/skills/bi-performance/scripts/install-tabular-editor.ps1 +90 -0
- package/skills/bi-performance/scripts/run-bpa.ps1 +161 -0
- package/skills/bi-performance/scripts/update-check.js +403 -0
- package/skills/bi-start/SKILL.md +40 -28
- package/skills/bi-start/scripts/update-check.js +1 -1
- package/src/content/base.md +15 -10
- package/src/content/routing.md +15 -18
- package/src/content/skills/bi-connect.md +391 -0
- package/src/content/skills/bi-dax.md +358 -0
- package/src/content/skills/{project-kickoff.md → bi-kickoff.md} +75 -44
- package/src/content/skills/bi-modeling.md +368 -0
- package/src/content/skills/bi-performance/SKILL.md +428 -0
- package/src/content/skills/bi-performance/scripts/install-tabular-editor.ps1 +90 -0
- package/src/content/skills/bi-performance/scripts/run-bpa.ps1 +161 -0
- package/src/content/skills/bi-start.md +39 -27
- package/theme/BISuperpowers.json +3888 -0
- package/commands/pbi-connect.md +0 -253
- package/commands/report-design.md +0 -403
- package/skills/pbi-connect/SKILL.md +0 -255
- package/skills/report-design/SKILL.md +0 -405
- package/skills/report-design/references/cli-commands.md +0 -184
- package/skills/report-design/references/cli-setup.md +0 -101
- package/skills/report-design/references/close-write-open-pattern.md +0 -80
- package/skills/report-design/references/layouts/finance.md +0 -65
- package/skills/report-design/references/layouts/generic.md +0 -46
- package/skills/report-design/references/layouts/hr.md +0 -48
- package/skills/report-design/references/layouts/marketing.md +0 -45
- package/skills/report-design/references/layouts/operations.md +0 -44
- package/skills/report-design/references/layouts/sales.md +0 -50
- package/skills/report-design/references/native-visuals.md +0 -341
- package/skills/report-design/references/pbi-desktop-installation.md +0 -87
- package/skills/report-design/references/pbir-preview-activation.md +0 -40
- package/skills/report-design/references/slicer.md +0 -89
- package/skills/report-design/references/textbox.md +0 -101
- package/skills/report-design/references/themes/BISuperpowers.json +0 -915
- package/skills/report-design/references/troubleshooting.md +0 -135
- package/skills/report-design/references/visual-types.md +0 -78
- package/skills/report-design/scripts/apply-theme.js +0 -243
- package/skills/report-design/scripts/create-visual.js +0 -942
- package/skills/report-design/scripts/ensure-pbi-cli.sh +0 -41
- package/skills/report-design/scripts/validate-pbir.js +0 -351
- package/src/content/skills/pbi-connect.md +0 -226
- package/src/content/skills/report-design/SKILL.md +0 -376
- package/src/content/skills/report-design/references/cli-commands.md +0 -184
- package/src/content/skills/report-design/references/cli-setup.md +0 -101
- package/src/content/skills/report-design/references/close-write-open-pattern.md +0 -80
- package/src/content/skills/report-design/references/layouts/finance.md +0 -65
- package/src/content/skills/report-design/references/layouts/generic.md +0 -46
- package/src/content/skills/report-design/references/layouts/hr.md +0 -48
- package/src/content/skills/report-design/references/layouts/marketing.md +0 -45
- package/src/content/skills/report-design/references/layouts/operations.md +0 -44
- package/src/content/skills/report-design/references/layouts/sales.md +0 -50
- package/src/content/skills/report-design/references/native-visuals.md +0 -341
- package/src/content/skills/report-design/references/pbi-desktop-installation.md +0 -87
- package/src/content/skills/report-design/references/pbir-preview-activation.md +0 -40
- package/src/content/skills/report-design/references/slicer.md +0 -89
- package/src/content/skills/report-design/references/textbox.md +0 -101
- package/src/content/skills/report-design/references/themes/BISuperpowers.json +0 -915
- package/src/content/skills/report-design/references/troubleshooting.md +0 -135
- package/src/content/skills/report-design/references/visual-types.md +0 -78
- package/src/content/skills/report-design/scripts/apply-theme.js +0 -243
- package/src/content/skills/report-design/scripts/create-visual.js +0 -942
- package/src/content/skills/report-design/scripts/ensure-pbi-cli.sh +0 -41
- package/src/content/skills/report-design/scripts/validate-pbir.js +0 -351
package/bin/commands/install.js
CHANGED
|
@@ -18,10 +18,8 @@
|
|
|
18
18
|
* expected config file in the format that agent requires (JSON for
|
|
19
19
|
* most, TOML for Codex) — see `lib/mcp-config.js` for details.
|
|
20
20
|
*
|
|
21
|
-
* Fully open source (MIT).
|
|
22
|
-
*
|
|
23
|
-
* comments and JSDoc stay in English so contributors from any language
|
|
24
|
-
* can work on the source.
|
|
21
|
+
* Fully open source (MIT). CLI copy, comments, and JSDoc stay in English
|
|
22
|
+
* so contributors from any language can work on the source.
|
|
25
23
|
*
|
|
26
24
|
* Usage:
|
|
27
25
|
* npx @luquimbo/bi-superpowers install
|
|
@@ -89,8 +87,8 @@ async function selectMultiple(rl, items, preselected = []) {
|
|
|
89
87
|
console.log(` ${i + 1}) ${marker} ${item.name}`);
|
|
90
88
|
});
|
|
91
89
|
console.log();
|
|
92
|
-
console.log('
|
|
93
|
-
console.log('
|
|
90
|
+
console.log(' Enter numbers separated by commas (for example: 1,2,3)');
|
|
91
|
+
console.log(' Press Enter for detected agents, or "a" for all agents');
|
|
94
92
|
|
|
95
93
|
const answer = await prompt(rl, '\n > ');
|
|
96
94
|
|
|
@@ -161,11 +159,11 @@ function copySkillDir(srcDir, destDir) {
|
|
|
161
159
|
*/
|
|
162
160
|
function formatFsError(err, context) {
|
|
163
161
|
const codeHints = {
|
|
164
|
-
EACCES: '
|
|
165
|
-
EPERM: '
|
|
166
|
-
ENOSPC: 'No
|
|
167
|
-
ENOENT: '
|
|
168
|
-
EROFS: '
|
|
162
|
+
EACCES: 'Permission denied. Check the directory permissions.',
|
|
163
|
+
EPERM: 'Operation not permitted. On Windows, try running as Administrator.',
|
|
164
|
+
ENOSPC: 'No disk space left.',
|
|
165
|
+
ENOENT: 'File or directory does not exist.',
|
|
166
|
+
EROFS: 'Read-only filesystem.',
|
|
169
167
|
};
|
|
170
168
|
const hint = codeHints[err.code] || '';
|
|
171
169
|
return `${context}: ${err.message}${hint ? `\n ${hint}` : ''}`;
|
|
@@ -188,7 +186,9 @@ function parseArgs(args) {
|
|
|
188
186
|
const next = args[i + 1];
|
|
189
187
|
if (next === undefined || next.startsWith('-')) {
|
|
190
188
|
// Missing value — warn and skip this flag instead of crashing.
|
|
191
|
-
console.warn(
|
|
189
|
+
console.warn(
|
|
190
|
+
`⚠ Flag ${args[i]} is missing a value. Usage: ${args[i]} <agent-id>. Ignoring.`
|
|
191
|
+
);
|
|
192
192
|
continue;
|
|
193
193
|
}
|
|
194
194
|
opts.agentFlags.push(next);
|
|
@@ -212,8 +212,8 @@ async function resolveSelectedAgents(opts, baseDir, chalk) {
|
|
|
212
212
|
const known = opts.agentFlags.filter((a) => AGENTS[a]);
|
|
213
213
|
const unknown = opts.agentFlags.filter((a) => !AGENTS[a]);
|
|
214
214
|
if (unknown.length > 0) {
|
|
215
|
-
console.log(chalk.yellow(`
|
|
216
|
-
console.log(chalk.gray(`
|
|
215
|
+
console.log(chalk.yellow(` Unknown agents: ${unknown.join(', ')}`));
|
|
216
|
+
console.log(chalk.gray(` Available agents: ${Object.keys(AGENTS).join(', ')}\n`));
|
|
217
217
|
}
|
|
218
218
|
return known;
|
|
219
219
|
}
|
|
@@ -224,11 +224,11 @@ async function resolveSelectedAgents(opts, baseDir, chalk) {
|
|
|
224
224
|
|
|
225
225
|
// Interactive mode — detect installed agents and prompt the user.
|
|
226
226
|
const detected = detectAgents(baseDir);
|
|
227
|
-
console.log(chalk.cyan('
|
|
227
|
+
console.log(chalk.cyan(' Select the agents where you want to install:\n'));
|
|
228
228
|
|
|
229
229
|
const items = Object.entries(AGENTS).map(([id, agent]) => ({
|
|
230
230
|
id,
|
|
231
|
-
name: detected.includes(id) ? `${agent.name} ${chalk.green('(
|
|
231
|
+
name: detected.includes(id) ? `${agent.name} ${chalk.green('(detected)')}` : agent.name,
|
|
232
232
|
}));
|
|
233
233
|
|
|
234
234
|
const rl = createReadline();
|
|
@@ -330,9 +330,7 @@ function performInstall(skillsSourceDir, skillDirs, selectedAgents, baseDir) {
|
|
|
330
330
|
* @returns {Array<{agent: string, success: boolean, configPath?: string, error?: string}>}
|
|
331
331
|
*/
|
|
332
332
|
function configureMcpsForAgents(selectedAgents, packageDir, baseDir, chalk) {
|
|
333
|
-
console.log(
|
|
334
|
-
chalk.cyan('\n Configurando MCP servers (Power BI Modeling + Microsoft Learn)...\n')
|
|
335
|
-
);
|
|
333
|
+
console.log(chalk.cyan('\n Configuring MCP servers (Power BI Modeling + Microsoft Learn)...\n'));
|
|
336
334
|
|
|
337
335
|
const results = [];
|
|
338
336
|
for (const agentId of selectedAgents) {
|
|
@@ -374,7 +372,7 @@ async function installCommand(args, config) {
|
|
|
374
372
|
if (!fs.existsSync(skillsSourceDir)) {
|
|
375
373
|
console.error(
|
|
376
374
|
chalk.red(
|
|
377
|
-
'
|
|
375
|
+
'Skills directory not found. Reinstall the package with: npm install -g @luquimbo/bi-superpowers'
|
|
378
376
|
)
|
|
379
377
|
);
|
|
380
378
|
process.exit(1);
|
|
@@ -390,7 +388,7 @@ async function installCommand(args, config) {
|
|
|
390
388
|
)
|
|
391
389
|
.map((d) => d.name);
|
|
392
390
|
} catch (err) {
|
|
393
|
-
console.error(chalk.red(formatFsError(err, '
|
|
391
|
+
console.error(chalk.red(formatFsError(err, 'Could not read skills')));
|
|
394
392
|
process.exit(1);
|
|
395
393
|
}
|
|
396
394
|
|
|
@@ -400,7 +398,7 @@ async function installCommand(args, config) {
|
|
|
400
398
|
chalk.bold.cyan('BI Agent Superpowers') +
|
|
401
399
|
chalk.gray(` v${config.version}`) +
|
|
402
400
|
'\n' +
|
|
403
|
-
chalk.gray('
|
|
401
|
+
chalk.gray('Multi-agent installer'),
|
|
404
402
|
{
|
|
405
403
|
padding: 1,
|
|
406
404
|
borderStyle: 'round',
|
|
@@ -409,21 +407,19 @@ async function installCommand(args, config) {
|
|
|
409
407
|
)
|
|
410
408
|
);
|
|
411
409
|
|
|
412
|
-
console.log(chalk.gray(`
|
|
413
|
-
console.log(chalk.gray(` Skills: ${skillDirs.length}
|
|
410
|
+
console.log(chalk.gray(` Install path: ~/${UNIVERSAL_DIR}/`));
|
|
411
|
+
console.log(chalk.gray(` Skills available: ${skillDirs.length}\n`));
|
|
414
412
|
|
|
415
413
|
// Resolve which agents to configure.
|
|
416
414
|
const selectedAgents = await resolveSelectedAgents(opts, baseDir, chalk);
|
|
417
415
|
|
|
418
416
|
if (selectedAgents.length === 0) {
|
|
419
|
-
console.log(chalk.yellow('\n
|
|
417
|
+
console.log(chalk.yellow('\n No agent selected. Nothing to install.'));
|
|
420
418
|
return;
|
|
421
419
|
}
|
|
422
420
|
|
|
423
421
|
console.log(
|
|
424
|
-
chalk.cyan(
|
|
425
|
-
`\n Instalando ${skillDirs.length} skills para ${selectedAgents.length} agentes...\n`
|
|
426
|
-
)
|
|
422
|
+
chalk.cyan(`\n Installing ${skillDirs.length} skills for ${selectedAgents.length} agents...\n`)
|
|
427
423
|
);
|
|
428
424
|
|
|
429
425
|
// Phase 1: copy skills and create symlinks per agent.
|
|
@@ -434,7 +430,7 @@ async function installCommand(args, config) {
|
|
|
434
430
|
agentResults = result.agentResults;
|
|
435
431
|
copyFallbacks = result.copyFallbacks;
|
|
436
432
|
} catch (err) {
|
|
437
|
-
console.error(chalk.red(formatFsError(err, '
|
|
433
|
+
console.error(chalk.red(formatFsError(err, 'Skill installation failed')));
|
|
438
434
|
process.exit(1);
|
|
439
435
|
}
|
|
440
436
|
|
|
@@ -455,9 +451,9 @@ async function installCommand(args, config) {
|
|
|
455
451
|
if (copyFallbacks > 0) {
|
|
456
452
|
console.log(
|
|
457
453
|
chalk.yellow(
|
|
458
|
-
`\n ⚠ ${copyFallbacks}
|
|
459
|
-
'(
|
|
460
|
-
" Re-
|
|
454
|
+
`\n ⚠ ${copyFallbacks} agent(s) used copy fallback instead of symlink ` +
|
|
455
|
+
'(probably Windows without admin permissions).\n' +
|
|
456
|
+
" Re-run 'super install' after each upgrade; copied installs are mirrored and stale runtimes are removed."
|
|
461
457
|
)
|
|
462
458
|
);
|
|
463
459
|
}
|
|
@@ -471,36 +467,34 @@ async function installCommand(args, config) {
|
|
|
471
467
|
const mcpFailures = mcpResults.filter((r) => !r.success);
|
|
472
468
|
const hasFailures = mcpFailures.length > 0;
|
|
473
469
|
|
|
474
|
-
const successMsg = `
|
|
475
|
-
const failureMsg = `
|
|
470
|
+
const successMsg = `Installed ${skillDirs.length} skills + 2 MCPs for ${totalAgents} agents`;
|
|
471
|
+
const failureMsg = `Installed ${skillDirs.length} skills. MCPs: ${mcpSuccess}/${mcpResults.length} agents ok, ${mcpFailures.length} failed.`;
|
|
476
472
|
const headerLine = hasFailures ? chalk.yellow.bold(failureMsg) : chalk.green.bold(successMsg);
|
|
477
473
|
|
|
478
474
|
const failureDetail = hasFailures
|
|
479
475
|
? '\n' +
|
|
480
|
-
chalk.red('
|
|
476
|
+
chalk.red('Agents with MCP errors:') +
|
|
481
477
|
'\n' +
|
|
482
478
|
mcpFailures.map((r) => chalk.red(` ✗ ${r.agent}: ${r.error}`)).join('\n') +
|
|
483
479
|
'\n'
|
|
484
480
|
: '';
|
|
485
481
|
|
|
482
|
+
const skillSummary = skillDirs
|
|
483
|
+
.map((skillName) => ` /${skillName.padEnd(15)} — ${describeSkill(skillName)}`)
|
|
484
|
+
.join('\n');
|
|
485
|
+
|
|
486
486
|
console.log(
|
|
487
487
|
boxen(
|
|
488
488
|
headerLine +
|
|
489
489
|
failureDetail +
|
|
490
490
|
'\n\n' +
|
|
491
|
-
chalk.gray(`MCPs
|
|
491
|
+
chalk.gray(`MCPs configured in ${mcpSuccess}/${mcpResults.length} agents.`) +
|
|
492
492
|
'\n' +
|
|
493
|
-
chalk.gray('
|
|
493
|
+
chalk.gray('Restart your AI agent so it loads the new MCP configuration.') +
|
|
494
494
|
'\n\n' +
|
|
495
|
-
chalk.gray(
|
|
496
|
-
'\n' +
|
|
497
|
-
chalk.gray(' /bi-start — Arrancar una sesión: menú + update + conexión') +
|
|
495
|
+
chalk.gray(`Skills available (${skillDirs.length}):`) +
|
|
498
496
|
'\n' +
|
|
499
|
-
chalk.gray(
|
|
500
|
-
'\n' +
|
|
501
|
-
chalk.gray(' /pbi-connect — Conectá tu agente a Power BI Desktop') +
|
|
502
|
-
'\n' +
|
|
503
|
-
chalk.gray(' /report-design — Generá reportes PBIR para Power BI Desktop (Windows)'),
|
|
497
|
+
chalk.gray(skillSummary),
|
|
504
498
|
{
|
|
505
499
|
padding: 1,
|
|
506
500
|
margin: { top: 1 },
|
|
@@ -526,3 +520,22 @@ module.exports.copySkillDir = copySkillDir;
|
|
|
526
520
|
module.exports.formatFsError = formatFsError;
|
|
527
521
|
module.exports.AGENTS = AGENTS;
|
|
528
522
|
module.exports.UNIVERSAL_DIR = UNIVERSAL_DIR;
|
|
523
|
+
|
|
524
|
+
function describeSkill(skillName) {
|
|
525
|
+
switch (skillName) {
|
|
526
|
+
case 'bi-start':
|
|
527
|
+
return 'Session opener, update check, and routing';
|
|
528
|
+
case 'bi-kickoff':
|
|
529
|
+
return 'Analyze or start a new BI project';
|
|
530
|
+
case 'bi-modeling':
|
|
531
|
+
return 'Design and audit semantic models';
|
|
532
|
+
case 'bi-dax':
|
|
533
|
+
return 'Write, debug, and optimize DAX';
|
|
534
|
+
case 'bi-performance':
|
|
535
|
+
return 'Performance profiling, VertiPaq, and BPA';
|
|
536
|
+
case 'bi-connect':
|
|
537
|
+
return 'Connect Power BI Desktop and guide DAX UDF work';
|
|
538
|
+
default:
|
|
539
|
+
return 'Specialized BI skill';
|
|
540
|
+
}
|
|
541
|
+
}
|
package/bin/commands/lint.js
CHANGED
|
@@ -366,7 +366,7 @@ function lintCommand(args, config) {
|
|
|
366
366
|
// Determine which files to lint. Use the shared skill loader so we
|
|
367
367
|
// catch both flat (`<name>.md`) and folder-based (`<name>/SKILL.md`)
|
|
368
368
|
// skills — a previous version filtered with `f.endsWith('.md')` and
|
|
369
|
-
// silently skipped every folder-based skill
|
|
369
|
+
// silently skipped every folder-based skill.
|
|
370
370
|
const allSkills = readSkillDirectory(skillsDir);
|
|
371
371
|
const skillsByName = new Map(allSkills.map((s) => [s.name, s]));
|
|
372
372
|
let filesToLint = [];
|
|
@@ -379,7 +379,7 @@ function lintCommand(args, config) {
|
|
|
379
379
|
return f;
|
|
380
380
|
}
|
|
381
381
|
// Look up by skill name. normalizeSkillName accepts `dax`,
|
|
382
|
-
// `dax.md`, `
|
|
382
|
+
// `dax.md`, `folder-skill`, or `folder-skill/SKILL.md`.
|
|
383
383
|
const skill = skillsByName.get(normalizeSkillName(f));
|
|
384
384
|
if (skill) return skill.path;
|
|
385
385
|
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Validation Command
|
|
3
|
+
* ==========================
|
|
4
|
+
*
|
|
5
|
+
* Validates public fixtures and private project references without copying
|
|
6
|
+
* customer repositories into this repo. Versioned descriptors live under
|
|
7
|
+
* validation/projects/*.json; private paths live in validation.local.json,
|
|
8
|
+
* which is intentionally ignored by git.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* super validate-projects
|
|
12
|
+
* super validate-projects --project template
|
|
13
|
+
* super validate-projects --root C:\path\to\bi-superpowers
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const DEFAULT_VALIDATION_DIR = 'validation';
|
|
20
|
+
const DEFAULT_LOCAL_CONFIG = 'validation.local.json';
|
|
21
|
+
|
|
22
|
+
function parseArgs(args) {
|
|
23
|
+
const options = {
|
|
24
|
+
rootDir: process.cwd(),
|
|
25
|
+
localConfigPath: null,
|
|
26
|
+
projectIds: [],
|
|
27
|
+
json: false,
|
|
28
|
+
list: false,
|
|
29
|
+
strict: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
for (let index = 0; index < args.length; index++) {
|
|
33
|
+
const arg = args[index];
|
|
34
|
+
|
|
35
|
+
if (arg === '--root') {
|
|
36
|
+
options.rootDir = readValue(args, ++index, arg);
|
|
37
|
+
} else if (arg === '--local') {
|
|
38
|
+
options.localConfigPath = readValue(args, ++index, arg);
|
|
39
|
+
} else if (arg === '--project' || arg === '-p') {
|
|
40
|
+
options.projectIds.push(readValue(args, ++index, arg));
|
|
41
|
+
} else if (arg === '--json') {
|
|
42
|
+
options.json = true;
|
|
43
|
+
} else if (arg === '--list') {
|
|
44
|
+
options.list = true;
|
|
45
|
+
} else if (arg === '--strict') {
|
|
46
|
+
options.strict = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
options.rootDir = path.resolve(options.rootDir);
|
|
51
|
+
if (options.localConfigPath) {
|
|
52
|
+
options.localConfigPath = path.resolve(options.localConfigPath);
|
|
53
|
+
}
|
|
54
|
+
return options;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readValue(args, index, flag) {
|
|
58
|
+
const value = args[index];
|
|
59
|
+
if (!value || value.startsWith('-')) {
|
|
60
|
+
throw new Error(`${flag} requires a value`);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readJsonStrict(filePath) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
68
|
+
} catch (err) {
|
|
69
|
+
throw new Error(`Cannot read JSON at ${filePath}: ${err.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getValidationProjectsDir(rootDir) {
|
|
74
|
+
return path.join(rootDir, DEFAULT_VALIDATION_DIR, 'projects');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getLocalConfigPath(rootDir, overridePath) {
|
|
78
|
+
return overridePath || path.join(rootDir, DEFAULT_LOCAL_CONFIG);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function loadProjectDescriptors(rootDir) {
|
|
82
|
+
const projectsDir = getValidationProjectsDir(rootDir);
|
|
83
|
+
if (!fs.existsSync(projectsDir)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Validation project descriptors not found at ${projectsDir}. ` +
|
|
86
|
+
'Run this command from the bi-superpowers source checkout or pass --root <repo>.'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const descriptorFiles = fs
|
|
91
|
+
.readdirSync(projectsDir)
|
|
92
|
+
.filter((file) => file.endsWith('.json'))
|
|
93
|
+
.sort();
|
|
94
|
+
|
|
95
|
+
if (descriptorFiles.length === 0) {
|
|
96
|
+
throw new Error(`No validation project descriptors found in ${projectsDir}.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return descriptorFiles.map((file) => {
|
|
100
|
+
const filePath = path.join(projectsDir, file);
|
|
101
|
+
const descriptor = readJsonStrict(filePath);
|
|
102
|
+
validateDescriptor(descriptor, filePath);
|
|
103
|
+
return { ...descriptor, descriptorPath: filePath };
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateDescriptor(descriptor, filePath) {
|
|
108
|
+
if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) {
|
|
109
|
+
throw new Error(`Project descriptor must be an object: ${filePath}`);
|
|
110
|
+
}
|
|
111
|
+
if (!isValidProjectId(descriptor.id)) {
|
|
112
|
+
throw new Error(`Project descriptor has invalid id at ${filePath}`);
|
|
113
|
+
}
|
|
114
|
+
if (!descriptor.name || typeof descriptor.name !== 'string') {
|
|
115
|
+
throw new Error(`Project descriptor ${descriptor.id} must include a name`);
|
|
116
|
+
}
|
|
117
|
+
if (descriptor.path != null && typeof descriptor.path !== 'string') {
|
|
118
|
+
throw new Error(`Project descriptor ${descriptor.id} path must be a string`);
|
|
119
|
+
}
|
|
120
|
+
if (descriptor.path != null && path.isAbsolute(descriptor.path)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Project descriptor ${descriptor.id} path must be relative. Put private absolute paths in validation.local.json.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (descriptor.skills != null && !isStringArray(descriptor.skills)) {
|
|
126
|
+
throw new Error(`Project descriptor ${descriptor.id} skills must be an array of strings`);
|
|
127
|
+
}
|
|
128
|
+
if (descriptor.checks != null && !Array.isArray(descriptor.checks)) {
|
|
129
|
+
throw new Error(`Project descriptor ${descriptor.id} checks must be an array`);
|
|
130
|
+
}
|
|
131
|
+
for (const check of descriptor.checks || []) {
|
|
132
|
+
if (!check || typeof check !== 'object' || Array.isArray(check)) {
|
|
133
|
+
throw new Error(`Project descriptor ${descriptor.id} has an invalid check`);
|
|
134
|
+
}
|
|
135
|
+
if (!check.name || typeof check.name !== 'string') {
|
|
136
|
+
throw new Error(`Project descriptor ${descriptor.id} has a check without a name`);
|
|
137
|
+
}
|
|
138
|
+
if (!check.path || typeof check.path !== 'string') {
|
|
139
|
+
throw new Error(`Project descriptor ${descriptor.id} check ${check.name} needs a path`);
|
|
140
|
+
}
|
|
141
|
+
if (path.isAbsolute(check.path)) {
|
|
142
|
+
throw new Error(`Project descriptor ${descriptor.id} check ${check.name} must be relative`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isValidProjectId(value) {
|
|
148
|
+
return typeof value === 'string' && /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isStringArray(value) {
|
|
152
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function loadLocalConfig(rootDir, overridePath) {
|
|
156
|
+
const localPath = getLocalConfigPath(rootDir, overridePath);
|
|
157
|
+
if (!fs.existsSync(localPath)) {
|
|
158
|
+
return { path: localPath, projects: {} };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const config = readJsonStrict(localPath);
|
|
162
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
163
|
+
throw new Error(`Local validation config must be an object: ${localPath}`);
|
|
164
|
+
}
|
|
165
|
+
if (config.projects == null) {
|
|
166
|
+
return { path: localPath, projects: {} };
|
|
167
|
+
}
|
|
168
|
+
if (typeof config.projects !== 'object' || Array.isArray(config.projects)) {
|
|
169
|
+
throw new Error(`Local validation config projects must be an object: ${localPath}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const [projectId, project] of Object.entries(config.projects)) {
|
|
173
|
+
if (!isValidProjectId(projectId)) {
|
|
174
|
+
throw new Error(`Local validation config has invalid project id: ${projectId}`);
|
|
175
|
+
}
|
|
176
|
+
if (!project || typeof project !== 'object' || Array.isArray(project)) {
|
|
177
|
+
throw new Error(`Local validation project ${projectId} must be an object`);
|
|
178
|
+
}
|
|
179
|
+
if (!project.path || typeof project.path !== 'string') {
|
|
180
|
+
throw new Error(`Local validation project ${projectId} must include a path`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { path: localPath, projects: config.projects };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function mergeProjects(descriptors, localProjects) {
|
|
188
|
+
const merged = new Map();
|
|
189
|
+
|
|
190
|
+
for (const descriptor of descriptors) {
|
|
191
|
+
const local = localProjects[descriptor.id] || {};
|
|
192
|
+
merged.set(descriptor.id, {
|
|
193
|
+
...descriptor,
|
|
194
|
+
path: local.path || descriptor.path,
|
|
195
|
+
localOnly: false,
|
|
196
|
+
localNotes: local.notes,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const [id, local] of Object.entries(localProjects)) {
|
|
201
|
+
if (merged.has(id)) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
merged.set(id, {
|
|
205
|
+
id,
|
|
206
|
+
name: local.name || id,
|
|
207
|
+
privacy: 'private-local',
|
|
208
|
+
path: local.path,
|
|
209
|
+
skills: local.skills || [],
|
|
210
|
+
checks: local.checks || [],
|
|
211
|
+
localOnly: true,
|
|
212
|
+
localNotes: local.notes,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return Array.from(merged.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function resolveProjectPath(rootDir, projectPath) {
|
|
220
|
+
if (!projectPath) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return path.isAbsolute(projectPath)
|
|
224
|
+
? path.normalize(projectPath)
|
|
225
|
+
: path.resolve(rootDir, projectPath);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function validateProject(project, rootDir) {
|
|
229
|
+
const resolvedPath = resolveProjectPath(rootDir, project.path);
|
|
230
|
+
const result = {
|
|
231
|
+
id: project.id,
|
|
232
|
+
name: project.name,
|
|
233
|
+
privacy: project.privacy || 'unspecified',
|
|
234
|
+
path: resolvedPath,
|
|
235
|
+
status: 'passed',
|
|
236
|
+
reason: null,
|
|
237
|
+
checks: [],
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
if (!resolvedPath) {
|
|
241
|
+
result.status = 'skipped';
|
|
242
|
+
result.reason = 'No path configured. Add this project to validation.local.json.';
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
247
|
+
result.status = 'failed';
|
|
248
|
+
result.reason = `Project path does not exist: ${resolvedPath}`;
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const check of project.checks || []) {
|
|
253
|
+
const checkPath = path.resolve(resolvedPath, check.path);
|
|
254
|
+
if (!isPathInside(resolvedPath, checkPath)) {
|
|
255
|
+
result.checks.push({
|
|
256
|
+
name: check.name,
|
|
257
|
+
path: checkPath,
|
|
258
|
+
passed: false,
|
|
259
|
+
reason: 'Check path escapes the project root.',
|
|
260
|
+
});
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const passed = fs.existsSync(checkPath);
|
|
264
|
+
result.checks.push({
|
|
265
|
+
name: check.name,
|
|
266
|
+
path: checkPath,
|
|
267
|
+
passed,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (result.checks.some((check) => !check.passed)) {
|
|
272
|
+
result.status = 'failed';
|
|
273
|
+
result.reason = 'One or more required project files are missing.';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isPathInside(parentPath, childPath) {
|
|
280
|
+
const relative = path.relative(parentPath, childPath);
|
|
281
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function runValidation(options) {
|
|
285
|
+
const descriptors = loadProjectDescriptors(options.rootDir);
|
|
286
|
+
const localConfig = loadLocalConfig(options.rootDir, options.localConfigPath);
|
|
287
|
+
const projects = mergeProjects(descriptors, localConfig.projects);
|
|
288
|
+
const selected = filterProjects(projects, options.projectIds);
|
|
289
|
+
const results = selected.projects.map((project) => validateProject(project, options.rootDir));
|
|
290
|
+
|
|
291
|
+
if (options.strict) {
|
|
292
|
+
for (const result of results) {
|
|
293
|
+
if (result.status === 'skipped') {
|
|
294
|
+
result.status = 'failed';
|
|
295
|
+
result.reason = `${result.reason} Strict mode treats skipped projects as failures.`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
rootDir: options.rootDir,
|
|
302
|
+
localConfigPath: localConfig.path,
|
|
303
|
+
missingProjectIds: selected.missingProjectIds,
|
|
304
|
+
results,
|
|
305
|
+
summary: summarizeResults(results, selected.missingProjectIds),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function filterProjects(projects, ids) {
|
|
310
|
+
if (ids.length === 0) {
|
|
311
|
+
return { projects, missingProjectIds: [] };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const byId = new Map(projects.map((project) => [project.id, project]));
|
|
315
|
+
const selected = [];
|
|
316
|
+
const missingProjectIds = [];
|
|
317
|
+
|
|
318
|
+
for (const id of ids) {
|
|
319
|
+
if (byId.has(id)) {
|
|
320
|
+
selected.push(byId.get(id));
|
|
321
|
+
} else {
|
|
322
|
+
missingProjectIds.push(id);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { projects: selected, missingProjectIds };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function summarizeResults(results, missingProjectIds = []) {
|
|
330
|
+
return {
|
|
331
|
+
total: results.length,
|
|
332
|
+
passed: results.filter((result) => result.status === 'passed').length,
|
|
333
|
+
failed:
|
|
334
|
+
results.filter((result) => result.status === 'failed').length + missingProjectIds.length,
|
|
335
|
+
skipped: results.filter((result) => result.status === 'skipped').length,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function printList(report) {
|
|
340
|
+
console.log('BI Agent Superpowers — Validation Projects');
|
|
341
|
+
console.log('==========================================\n');
|
|
342
|
+
|
|
343
|
+
if (report.results.length === 0) {
|
|
344
|
+
console.log('No project descriptors found.');
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for (const result of report.results) {
|
|
349
|
+
console.log(`- ${result.id}: ${result.name} (${result.privacy})`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function printReport(report) {
|
|
354
|
+
console.log('BI Agent Superpowers — Project Validation');
|
|
355
|
+
console.log('=========================================\n');
|
|
356
|
+
console.log(`Root: ${report.rootDir}`);
|
|
357
|
+
console.log(`Local config: ${report.localConfigPath}`);
|
|
358
|
+
console.log('');
|
|
359
|
+
|
|
360
|
+
for (const missingId of report.missingProjectIds) {
|
|
361
|
+
console.log(`FAIL ${missingId}`);
|
|
362
|
+
console.log(' Project id was requested but no descriptor or local config exists.\n');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const result of report.results) {
|
|
366
|
+
const label = result.status.toUpperCase();
|
|
367
|
+
console.log(`${label} ${result.id} — ${result.name}`);
|
|
368
|
+
if (result.path) {
|
|
369
|
+
console.log(` Path: ${result.path}`);
|
|
370
|
+
}
|
|
371
|
+
if (result.reason) {
|
|
372
|
+
console.log(` ${result.reason}`);
|
|
373
|
+
}
|
|
374
|
+
for (const check of result.checks) {
|
|
375
|
+
const checkLabel = check.passed ? 'OK' : 'MISS';
|
|
376
|
+
console.log(` ${checkLabel} ${check.name}: ${check.path}`);
|
|
377
|
+
}
|
|
378
|
+
console.log('');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const summary = report.summary;
|
|
382
|
+
console.log(
|
|
383
|
+
`Summary: ${summary.passed} passed, ${summary.failed} failed, ${summary.skipped} skipped (${summary.total} checked)`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function validateProjectsCommand(args) {
|
|
388
|
+
let options;
|
|
389
|
+
try {
|
|
390
|
+
options = parseArgs(args || []);
|
|
391
|
+
const report = runValidation(options);
|
|
392
|
+
|
|
393
|
+
if (options.json) {
|
|
394
|
+
console.log(JSON.stringify(report, null, 2));
|
|
395
|
+
} else if (options.list) {
|
|
396
|
+
printList(report);
|
|
397
|
+
} else {
|
|
398
|
+
printReport(report);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (report.summary.failed > 0) {
|
|
402
|
+
process.exitCode = 1;
|
|
403
|
+
}
|
|
404
|
+
} catch (err) {
|
|
405
|
+
if (options && options.json) {
|
|
406
|
+
console.log(JSON.stringify({ error: err.message }, null, 2));
|
|
407
|
+
} else {
|
|
408
|
+
console.error(`Error: ${err.message}`);
|
|
409
|
+
}
|
|
410
|
+
process.exitCode = 1;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = Object.assign(validateProjectsCommand, {
|
|
415
|
+
parseArgs,
|
|
416
|
+
readJsonStrict,
|
|
417
|
+
loadProjectDescriptors,
|
|
418
|
+
loadLocalConfig,
|
|
419
|
+
mergeProjects,
|
|
420
|
+
resolveProjectPath,
|
|
421
|
+
validateProject,
|
|
422
|
+
isPathInside,
|
|
423
|
+
runValidation,
|
|
424
|
+
summarizeResults,
|
|
425
|
+
});
|