@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.
Files changed (100) hide show
  1. package/.claude-plugin/marketplace.json +8 -6
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.claude-plugin/skill-manifest.json +35 -19
  4. package/.plugin/plugin.json +1 -1
  5. package/AGENTS.md +150 -26
  6. package/CHANGELOG.md +489 -14
  7. package/README.md +103 -114
  8. package/bin/cli.js +7 -1
  9. package/bin/commands/diff.js +2 -2
  10. package/bin/commands/install.js +58 -45
  11. package/bin/commands/lint.js +2 -2
  12. package/bin/commands/validate-projects.js +425 -0
  13. package/bin/lib/generators/claude-plugin.js +31 -7
  14. package/bin/lib/generators/shared.js +11 -7
  15. package/bin/lib/mcp-config.js +22 -2
  16. package/bin/lib/skills.js +8 -8
  17. package/bin/mcp/powerbi-modeling-launcher.js +8 -4
  18. package/bin/postinstall.js +14 -12
  19. package/bin/utils/mcp-detect.js +11 -11
  20. package/commands/bi-connect.md +418 -0
  21. package/commands/bi-dax.md +385 -0
  22. package/commands/{project-kickoff.md → bi-kickoff.md} +78 -47
  23. package/commands/bi-modeling.md +395 -0
  24. package/commands/bi-performance.md +455 -0
  25. package/commands/bi-start.md +39 -27
  26. package/desktop-extension/manifest.json +2 -2
  27. package/package.json +3 -2
  28. package/skills/bi-connect/SKILL.md +420 -0
  29. package/skills/{pbi-connect → bi-connect}/scripts/update-check.js +1 -1
  30. package/skills/bi-dax/SKILL.md +387 -0
  31. package/skills/{report-design → bi-dax}/scripts/update-check.js +1 -1
  32. package/skills/{project-kickoff → bi-kickoff}/SKILL.md +79 -48
  33. package/skills/{project-kickoff → bi-kickoff}/scripts/update-check.js +1 -1
  34. package/skills/bi-modeling/SKILL.md +397 -0
  35. package/skills/bi-modeling/scripts/update-check.js +403 -0
  36. package/skills/bi-performance/SKILL.md +457 -0
  37. package/skills/bi-performance/scripts/install-tabular-editor.ps1 +90 -0
  38. package/skills/bi-performance/scripts/run-bpa.ps1 +161 -0
  39. package/skills/bi-performance/scripts/update-check.js +403 -0
  40. package/skills/bi-start/SKILL.md +40 -28
  41. package/skills/bi-start/scripts/update-check.js +1 -1
  42. package/src/content/base.md +15 -10
  43. package/src/content/routing.md +15 -18
  44. package/src/content/skills/bi-connect.md +391 -0
  45. package/src/content/skills/bi-dax.md +358 -0
  46. package/src/content/skills/{project-kickoff.md → bi-kickoff.md} +75 -44
  47. package/src/content/skills/bi-modeling.md +368 -0
  48. package/src/content/skills/bi-performance/SKILL.md +428 -0
  49. package/src/content/skills/bi-performance/scripts/install-tabular-editor.ps1 +90 -0
  50. package/src/content/skills/bi-performance/scripts/run-bpa.ps1 +161 -0
  51. package/src/content/skills/bi-start.md +39 -27
  52. package/theme/BISuperpowers.json +3888 -0
  53. package/commands/pbi-connect.md +0 -253
  54. package/commands/report-design.md +0 -403
  55. package/skills/pbi-connect/SKILL.md +0 -255
  56. package/skills/report-design/SKILL.md +0 -405
  57. package/skills/report-design/references/cli-commands.md +0 -184
  58. package/skills/report-design/references/cli-setup.md +0 -101
  59. package/skills/report-design/references/close-write-open-pattern.md +0 -80
  60. package/skills/report-design/references/layouts/finance.md +0 -65
  61. package/skills/report-design/references/layouts/generic.md +0 -46
  62. package/skills/report-design/references/layouts/hr.md +0 -48
  63. package/skills/report-design/references/layouts/marketing.md +0 -45
  64. package/skills/report-design/references/layouts/operations.md +0 -44
  65. package/skills/report-design/references/layouts/sales.md +0 -50
  66. package/skills/report-design/references/native-visuals.md +0 -341
  67. package/skills/report-design/references/pbi-desktop-installation.md +0 -87
  68. package/skills/report-design/references/pbir-preview-activation.md +0 -40
  69. package/skills/report-design/references/slicer.md +0 -89
  70. package/skills/report-design/references/textbox.md +0 -101
  71. package/skills/report-design/references/themes/BISuperpowers.json +0 -915
  72. package/skills/report-design/references/troubleshooting.md +0 -135
  73. package/skills/report-design/references/visual-types.md +0 -78
  74. package/skills/report-design/scripts/apply-theme.js +0 -243
  75. package/skills/report-design/scripts/create-visual.js +0 -942
  76. package/skills/report-design/scripts/ensure-pbi-cli.sh +0 -41
  77. package/skills/report-design/scripts/validate-pbir.js +0 -351
  78. package/src/content/skills/pbi-connect.md +0 -226
  79. package/src/content/skills/report-design/SKILL.md +0 -376
  80. package/src/content/skills/report-design/references/cli-commands.md +0 -184
  81. package/src/content/skills/report-design/references/cli-setup.md +0 -101
  82. package/src/content/skills/report-design/references/close-write-open-pattern.md +0 -80
  83. package/src/content/skills/report-design/references/layouts/finance.md +0 -65
  84. package/src/content/skills/report-design/references/layouts/generic.md +0 -46
  85. package/src/content/skills/report-design/references/layouts/hr.md +0 -48
  86. package/src/content/skills/report-design/references/layouts/marketing.md +0 -45
  87. package/src/content/skills/report-design/references/layouts/operations.md +0 -44
  88. package/src/content/skills/report-design/references/layouts/sales.md +0 -50
  89. package/src/content/skills/report-design/references/native-visuals.md +0 -341
  90. package/src/content/skills/report-design/references/pbi-desktop-installation.md +0 -87
  91. package/src/content/skills/report-design/references/pbir-preview-activation.md +0 -40
  92. package/src/content/skills/report-design/references/slicer.md +0 -89
  93. package/src/content/skills/report-design/references/textbox.md +0 -101
  94. package/src/content/skills/report-design/references/themes/BISuperpowers.json +0 -915
  95. package/src/content/skills/report-design/references/troubleshooting.md +0 -135
  96. package/src/content/skills/report-design/references/visual-types.md +0 -78
  97. package/src/content/skills/report-design/scripts/apply-theme.js +0 -243
  98. package/src/content/skills/report-design/scripts/create-visual.js +0 -942
  99. package/src/content/skills/report-design/scripts/ensure-pbi-cli.sh +0 -41
  100. package/src/content/skills/report-design/scripts/validate-pbir.js +0 -351
@@ -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). The user-facing messages are in Spanish to
22
- * match the primary Spanish-speaking audience of the project; code
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(' Ingresá números separados por comas (ej: 1,2,3)');
93
- console.log(' Presioná Enter para los detectados, o "a" para todos');
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: 'Permiso denegado. Revisá los permisos del directorio.',
165
- EPERM: 'Operación no permitida. En Windows, probá ejecutar como Administrador.',
166
- ENOSPC: 'No hay espacio en disco.',
167
- ENOENT: 'Archivo o directorio no existe.',
168
- EROFS: 'Sistema de archivos en solo lectura.',
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(`⚠ Flag ${args[i]} sin valor. Uso: ${args[i]} <agente-id>. Ignorando.`);
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(` Agentes desconocidos: ${unknown.join(', ')}`));
216
- console.log(chalk.gray(` Disponibles: ${Object.keys(AGENTS).join(', ')}\n`));
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(' Seleccioná los agentes donde querés instalar:\n'));
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('(detectado)')}` : agent.name,
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
- 'Directorio de skills no encontrado. Reinstalá el paquete con: npm install -g @luquimbo/bi-superpowers'
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, 'No pude leer los skills')));
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('Instalador multi-agente'),
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(` Ruta de instalación: ~/${UNIVERSAL_DIR}/`));
413
- console.log(chalk.gray(` Skills: ${skillDirs.length} disponibles\n`));
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 Ningún agente seleccionado. Nada que instalar.'));
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, 'Falló la instalación de skills')));
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} agente(s) usaron copia en vez de symlink ` +
459
- '(probable Windows sin permisos de admin).\n' +
460
- " Re-ejecutá 'super install' tras cada upgrade; la copia se espeja y elimina runtimes viejos."
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 = `Instalados ${skillDirs.length} skills + 2 MCPs para ${totalAgents} agentes`;
475
- const failureMsg = `Instalados ${skillDirs.length} skills. MCPs: ${mcpSuccess}/${mcpResults.length} agentes ✓, ${mcpFailures.length} con errores.`;
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('Agentes con errores en MCP:') +
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 configurados en ${mcpSuccess}/${mcpResults.length} agentes.`) +
491
+ chalk.gray(`MCPs configured in ${mcpSuccess}/${mcpResults.length} agents.`) +
492
492
  '\n' +
493
- chalk.gray('Reiniciá tu agente AI para que tome los MCPs nuevos.') +
493
+ chalk.gray('Restart your AI agent so it loads the new MCP configuration.') +
494
494
  '\n\n' +
495
- chalk.gray('Los 4 skills disponibles:') +
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(' /project-kickoff — Analizá tu proyecto BI (proyecto nuevo)') +
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
+ }
@@ -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 (e.g. `report-design`).
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`, `report-design`, or `report-design/SKILL.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
+ });