@luquimbo/bi-superpowers 1.0.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.
Files changed (193) hide show
  1. package/.claude-plugin/plugin.json +8 -0
  2. package/.mcp.json +25 -0
  3. package/AGENTS.md +244 -0
  4. package/CHANGELOG.md +265 -0
  5. package/LICENSE +21 -0
  6. package/README.md +211 -0
  7. package/bin/build-plugin.js +30 -0
  8. package/bin/cli.js +1064 -0
  9. package/bin/commands/add.js +533 -0
  10. package/bin/commands/add.test.js +77 -0
  11. package/bin/commands/build-desktop.js +166 -0
  12. package/bin/commands/changelog.js +443 -0
  13. package/bin/commands/diff.js +325 -0
  14. package/bin/commands/lint.js +419 -0
  15. package/bin/commands/lint.test.js +103 -0
  16. package/bin/commands/mcp-setup.js +246 -0
  17. package/bin/commands/pull.js +287 -0
  18. package/bin/commands/pull.test.js +36 -0
  19. package/bin/commands/push.js +231 -0
  20. package/bin/commands/push.test.js +14 -0
  21. package/bin/commands/search.js +344 -0
  22. package/bin/commands/search.test.js +115 -0
  23. package/bin/commands/setup.js +545 -0
  24. package/bin/commands/setup.test.js +46 -0
  25. package/bin/commands/sync-profile.js +405 -0
  26. package/bin/commands/sync-profile.test.js +14 -0
  27. package/bin/commands/sync-source.js +418 -0
  28. package/bin/commands/sync-source.test.js +14 -0
  29. package/bin/commands/watch.js +206 -0
  30. package/bin/lib/generators/claude-plugin.js +266 -0
  31. package/bin/lib/generators/claude-plugin.test.js +110 -0
  32. package/bin/lib/generators/index.js +116 -0
  33. package/bin/lib/generators/shared.js +282 -0
  34. package/bin/lib/licensing/index.js +35 -0
  35. package/bin/lib/licensing/storage.js +364 -0
  36. package/bin/lib/licensing/storage.test.js +55 -0
  37. package/bin/lib/licensing/validator.js +213 -0
  38. package/bin/lib/licensing/validator.test.js +137 -0
  39. package/bin/lib/microsoft-mcp.js +176 -0
  40. package/bin/lib/microsoft-mcp.test.js +106 -0
  41. package/bin/lib/skills.js +84 -0
  42. package/bin/mcp/powerbi-modeling-launcher.js +38 -0
  43. package/bin/postinstall.js +44 -0
  44. package/bin/utils/errors.js +159 -0
  45. package/bin/utils/git.js +298 -0
  46. package/bin/utils/logger.js +142 -0
  47. package/bin/utils/mcp-detect.js +274 -0
  48. package/bin/utils/mcp-detect.test.js +105 -0
  49. package/bin/utils/pbix.js +305 -0
  50. package/bin/utils/pbix.test.js +37 -0
  51. package/bin/utils/profiles.js +312 -0
  52. package/bin/utils/projects.js +168 -0
  53. package/bin/utils/readline.js +206 -0
  54. package/bin/utils/readline.test.js +47 -0
  55. package/bin/utils/tui.js +314 -0
  56. package/bin/utils/tui.test.js +127 -0
  57. package/commands/contributions.md +265 -0
  58. package/commands/data-model-design.md +468 -0
  59. package/commands/dax-doctor.md +248 -0
  60. package/commands/fabric-scripts.md +452 -0
  61. package/commands/migration-assistant.md +290 -0
  62. package/commands/model-documenter.md +242 -0
  63. package/commands/pbi-connect.md +239 -0
  64. package/commands/project-kickoff.md +905 -0
  65. package/commands/report-layout.md +296 -0
  66. package/commands/rls-design.md +533 -0
  67. package/commands/theme-tweaker.md +624 -0
  68. package/config.example.json +23 -0
  69. package/config.json +23 -0
  70. package/desktop-extension/manifest.json +37 -0
  71. package/desktop-extension/package.json +10 -0
  72. package/desktop-extension/server.js +95 -0
  73. package/docs/openrouter-free-models.md +92 -0
  74. package/library/examples/README.md +151 -0
  75. package/library/examples/finance-reporting/README.md +351 -0
  76. package/library/examples/finance-reporting/data-model.md +267 -0
  77. package/library/examples/finance-reporting/measures.dax +557 -0
  78. package/library/examples/hr-analytics/README.md +371 -0
  79. package/library/examples/hr-analytics/data-model.md +315 -0
  80. package/library/examples/hr-analytics/measures.dax +460 -0
  81. package/library/examples/marketing-analytics/README.md +37 -0
  82. package/library/examples/marketing-analytics/data-model.md +62 -0
  83. package/library/examples/marketing-analytics/measures.dax +110 -0
  84. package/library/examples/retail-analytics/README.md +439 -0
  85. package/library/examples/retail-analytics/data-model.md +288 -0
  86. package/library/examples/retail-analytics/measures.dax +481 -0
  87. package/library/examples/supply-chain/README.md +37 -0
  88. package/library/examples/supply-chain/data-model.md +69 -0
  89. package/library/examples/supply-chain/measures.dax +77 -0
  90. package/library/examples/udf-library/README.md +228 -0
  91. package/library/examples/udf-library/functions.dax +571 -0
  92. package/library/snippets/dax/README.md +292 -0
  93. package/library/snippets/dax/business-domains.md +576 -0
  94. package/library/snippets/dax/calculate-patterns.md +276 -0
  95. package/library/snippets/dax/calculation-groups.md +489 -0
  96. package/library/snippets/dax/error-handling.md +495 -0
  97. package/library/snippets/dax/iterators-and-aggregations.md +474 -0
  98. package/library/snippets/dax/kpis-and-metrics.md +293 -0
  99. package/library/snippets/dax/rankings-and-topn.md +235 -0
  100. package/library/snippets/dax/security-patterns.md +413 -0
  101. package/library/snippets/dax/text-and-formatting.md +316 -0
  102. package/library/snippets/dax/time-intelligence.md +196 -0
  103. package/library/snippets/dax/user-defined-functions.md +477 -0
  104. package/library/snippets/dax/virtual-tables.md +546 -0
  105. package/library/snippets/excel-formulas/README.md +84 -0
  106. package/library/snippets/excel-formulas/aggregations.md +330 -0
  107. package/library/snippets/excel-formulas/dates-and-times.md +361 -0
  108. package/library/snippets/excel-formulas/dynamic-arrays.md +314 -0
  109. package/library/snippets/excel-formulas/lookups.md +169 -0
  110. package/library/snippets/excel-formulas/text-functions.md +363 -0
  111. package/library/snippets/governance/naming-conventions.md +97 -0
  112. package/library/snippets/governance/review-checklists.md +107 -0
  113. package/library/snippets/power-query/README.md +389 -0
  114. package/library/snippets/power-query/api-integration.md +707 -0
  115. package/library/snippets/power-query/connections.md +434 -0
  116. package/library/snippets/power-query/data-cleaning.md +298 -0
  117. package/library/snippets/power-query/error-handling.md +526 -0
  118. package/library/snippets/power-query/parameters.md +350 -0
  119. package/library/snippets/power-query/performance.md +506 -0
  120. package/library/snippets/power-query/transformations.md +330 -0
  121. package/library/snippets/report-design/accessibility.md +78 -0
  122. package/library/snippets/report-design/chart-selection.md +54 -0
  123. package/library/snippets/report-design/layout-patterns.md +87 -0
  124. package/library/templates/data-models/README.md +93 -0
  125. package/library/templates/data-models/finance-model.md +627 -0
  126. package/library/templates/data-models/retail-star-schema.md +473 -0
  127. package/library/templates/excel/README.md +83 -0
  128. package/library/templates/excel/budget-tracker.md +432 -0
  129. package/library/templates/excel/data-entry-form.md +533 -0
  130. package/library/templates/power-bi/README.md +72 -0
  131. package/library/templates/power-bi/finance-report.md +449 -0
  132. package/library/templates/power-bi/kpi-scorecard.md +461 -0
  133. package/library/templates/power-bi/sales-dashboard.md +281 -0
  134. package/library/themes/excel/README.md +436 -0
  135. package/library/themes/power-bi/README.md +271 -0
  136. package/library/themes/power-bi/accessible.json +307 -0
  137. package/library/themes/power-bi/bi-superpowers-default.json +858 -0
  138. package/library/themes/power-bi/corporate-blue.json +291 -0
  139. package/library/themes/power-bi/dark-mode.json +291 -0
  140. package/library/themes/power-bi/minimal.json +292 -0
  141. package/library/themes/power-bi/print-friendly.json +309 -0
  142. package/package.json +93 -0
  143. package/skills/contributions/SKILL.md +267 -0
  144. package/skills/data-model-design/SKILL.md +470 -0
  145. package/skills/data-modeling/SKILL.md +254 -0
  146. package/skills/data-quality/SKILL.md +664 -0
  147. package/skills/dax/SKILL.md +708 -0
  148. package/skills/dax-doctor/SKILL.md +250 -0
  149. package/skills/dax-udf/SKILL.md +489 -0
  150. package/skills/deployment/SKILL.md +320 -0
  151. package/skills/excel-formulas/SKILL.md +463 -0
  152. package/skills/fabric-scripts/SKILL.md +454 -0
  153. package/skills/fast-standard/SKILL.md +509 -0
  154. package/skills/governance/SKILL.md +205 -0
  155. package/skills/migration-assistant/SKILL.md +292 -0
  156. package/skills/model-documenter/SKILL.md +244 -0
  157. package/skills/pbi-connect/SKILL.md +241 -0
  158. package/skills/power-query/SKILL.md +406 -0
  159. package/skills/project-kickoff/SKILL.md +907 -0
  160. package/skills/query-performance/SKILL.md +480 -0
  161. package/skills/report-design/SKILL.md +207 -0
  162. package/skills/report-layout/SKILL.md +298 -0
  163. package/skills/rls-design/SKILL.md +535 -0
  164. package/skills/semantic-model/SKILL.md +237 -0
  165. package/skills/testing-validation/SKILL.md +643 -0
  166. package/skills/theme-tweaker/SKILL.md +626 -0
  167. package/src/content/base.md +237 -0
  168. package/src/content/mcp-requirements.json +69 -0
  169. package/src/content/routing.md +203 -0
  170. package/src/content/skills/contributions.md +259 -0
  171. package/src/content/skills/data-model-design.md +462 -0
  172. package/src/content/skills/data-modeling.md +246 -0
  173. package/src/content/skills/data-quality.md +656 -0
  174. package/src/content/skills/dax-doctor.md +242 -0
  175. package/src/content/skills/dax-udf.md +481 -0
  176. package/src/content/skills/dax.md +700 -0
  177. package/src/content/skills/deployment.md +312 -0
  178. package/src/content/skills/excel-formulas.md +455 -0
  179. package/src/content/skills/fabric-scripts.md +446 -0
  180. package/src/content/skills/fast-standard.md +501 -0
  181. package/src/content/skills/governance.md +197 -0
  182. package/src/content/skills/migration-assistant.md +284 -0
  183. package/src/content/skills/model-documenter.md +236 -0
  184. package/src/content/skills/pbi-connect.md +233 -0
  185. package/src/content/skills/power-query.md +398 -0
  186. package/src/content/skills/project-kickoff.md +899 -0
  187. package/src/content/skills/query-performance.md +472 -0
  188. package/src/content/skills/report-design.md +199 -0
  189. package/src/content/skills/report-layout.md +290 -0
  190. package/src/content/skills/rls-design.md +527 -0
  191. package/src/content/skills/semantic-model.md +229 -0
  192. package/src/content/skills/testing-validation.md +635 -0
  193. package/src/content/skills/theme-tweaker.md +618 -0
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Push Command - Apply Repo Changes to Original File
3
+ * ====================================================
4
+ *
5
+ * Pushes changes from the repo back to the original BI file.
6
+ * This is a Phase 2 feature - currently provides guidance for PBIP workflow.
7
+ *
8
+ * Usage:
9
+ * super push Push current project
10
+ * super push <project-name> Push specific project
11
+ *
12
+ * @module commands/push
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const profiles = require('../utils/profiles');
19
+ const pbix = require('../utils/pbix');
20
+ const { getAllProjects, getProject, detectCurrentProject } = require('../utils/projects');
21
+
22
+ /**
23
+ * Parse command line arguments
24
+ * @param {string[]} args - CLI arguments
25
+ * @returns {Object} Parsed options
26
+ */
27
+ function parseArgs(args) {
28
+ const options = {
29
+ projectName: null,
30
+ force: false,
31
+ help: false,
32
+ };
33
+
34
+ for (let i = 0; i < args.length; i++) {
35
+ const arg = args[i];
36
+
37
+ if (arg === '--force' || arg === '-f') {
38
+ options.force = true;
39
+ } else if (arg === '--help' || arg === '-h') {
40
+ options.help = true;
41
+ } else if (!arg.startsWith('-') && !options.projectName) {
42
+ options.projectName = arg;
43
+ }
44
+ }
45
+
46
+ return options;
47
+ }
48
+
49
+ /**
50
+ * Show help message
51
+ */
52
+ function showHelp() {
53
+ console.log(`
54
+ super push - Aplicar cambios del repo al archivo original
55
+
56
+ Uso:
57
+ super push Push del proyecto actual
58
+ super push <nombre-proyecto> Push de un proyecto específico
59
+
60
+ Opciones:
61
+ --force, -f Forzar push aunque haya advertencias
62
+ --help, -h Mostrar esta ayuda
63
+
64
+ Nota:
65
+ Este comando actualmente soporta proyectos PBIP.
66
+ Para archivos .pbix, usa el workflow PBIP (recomendado).
67
+
68
+ Ejemplos:
69
+ super push sales-q4
70
+ super push --force
71
+ `);
72
+ }
73
+
74
+ // Note: getAllProjects, getProject, and detectCurrentProject are imported from ../utils/projects
75
+
76
+ /**
77
+ * Push changes for a PBIP project
78
+ * @param {Object} project - Project config
79
+ * @returns {Object} Result object
80
+ */
81
+ function pushPbipProject(project) {
82
+ const result = {
83
+ success: false,
84
+ message: '',
85
+ };
86
+
87
+ const sourcePath = project.source?.path;
88
+ if (!sourcePath) {
89
+ result.message = 'No source path configured';
90
+ return result;
91
+ }
92
+
93
+ const definitionPath = pbix.getPbipDefinitionPath(sourcePath);
94
+ if (!definitionPath) {
95
+ result.message = 'Could not find PBIP definition folder';
96
+ return result;
97
+ }
98
+
99
+ const repoDefinition = path.join(project.projectPath, 'definition');
100
+ if (!fs.existsSync(repoDefinition)) {
101
+ result.message = 'No definition folder in repo';
102
+ return result;
103
+ }
104
+
105
+ try {
106
+ // Copy files from repo to PBIP project
107
+ const files = pbix.copyDirectoryRecursive(repoDefinition, definitionPath);
108
+
109
+ // Update project.json with sync time
110
+ project.source.lastSync = new Date().toISOString();
111
+ const configPath = path.join(project.projectPath, 'project.json');
112
+ fs.writeFileSync(configPath, JSON.stringify(project, null, 2));
113
+
114
+ result.success = true;
115
+ result.message = `Pushed ${files.length} files to PBIP project`;
116
+ } catch (e) {
117
+ result.message = `Error: ${e.message}`;
118
+ }
119
+
120
+ return result;
121
+ }
122
+
123
+ /**
124
+ * Main push command handler
125
+ */
126
+ async function pushCommand(args, _config) {
127
+ const options = parseArgs(args);
128
+
129
+ if (options.help) {
130
+ showHelp();
131
+ return;
132
+ }
133
+
134
+ // Check if repo exists
135
+ const repoPath = profiles.getRepoPath();
136
+ if (!repoPath || !fs.existsSync(repoPath)) {
137
+ console.log(`
138
+ No se encontró el repositorio de BI.
139
+
140
+ Ejecuta primero:
141
+ super setup
142
+ `);
143
+ process.exit(1);
144
+ }
145
+
146
+ console.log(`
147
+ ════════════════════════════════════════════════════════════════
148
+ Push - Aplicar cambios del repo al archivo original
149
+ ════════════════════════════════════════════════════════════════
150
+ `);
151
+
152
+ let project;
153
+
154
+ if (options.projectName) {
155
+ project = getProject(repoPath, options.projectName);
156
+ if (!project) {
157
+ console.log(`✗ Proyecto no encontrado: ${options.projectName}`);
158
+ console.log('\nProyectos disponibles:');
159
+ getAllProjects(repoPath).forEach((p) => {
160
+ console.log(` - ${p.name}`);
161
+ });
162
+ process.exit(1);
163
+ }
164
+ } else {
165
+ project = detectCurrentProject(repoPath);
166
+ if (!project) {
167
+ console.log('Especifica un proyecto:\n');
168
+ console.log(' super push <nombre-proyecto>\n');
169
+ console.log('Proyectos disponibles:');
170
+ getAllProjects(repoPath).forEach((p) => {
171
+ console.log(` - ${p.name}`);
172
+ });
173
+ return;
174
+ }
175
+ }
176
+
177
+ console.log(`Proyecto: ${project.name}`);
178
+ console.log(`Tipo: ${project.type}`);
179
+ console.log(`Archivo original: ${project.source?.path || 'No configurado'}\n`);
180
+
181
+ // Handle based on project type
182
+ if (project.type === 'power-bi-project') {
183
+ // PBIP - we can push directly
184
+ const result = pushPbipProject(project);
185
+
186
+ if (result.success) {
187
+ console.log(`✓ ${result.message}`);
188
+ console.log(`
189
+ Los cambios se han aplicado al proyecto PBIP.
190
+ Abre Power BI Desktop y recarga el proyecto para ver los cambios.
191
+ `);
192
+ } else {
193
+ console.log(`✗ ${result.message}`);
194
+ }
195
+ } else if (project.type === 'power-bi') {
196
+ // PBIX - binary file, cannot push directly
197
+ console.log(`
198
+ ⚠ Los archivos .pbix son binarios y no se pueden modificar directamente.
199
+
200
+ Para aplicar cambios del repo al modelo, tienes estas opciones:
201
+
202
+ OPCIÓN 1: Convertir a PBIP (Recomendado)
203
+ 1. Abre el .pbix en Power BI Desktop
204
+ 2. File → Save as → Power BI Project (.pbip)
205
+ 3. Ejecuta: super add "ruta/al/proyecto.pbip"
206
+ 4. Los cambios del repo se pueden aplicar con: super push
207
+
208
+ OPCIÓN 2: Copiar manualmente
209
+ 1. Abre el .pbix en Power BI Desktop
210
+ 2. Copia las medidas/queries del repo manualmente
211
+ 3. Guarda el archivo
212
+
213
+ Los archivos del repo están en:
214
+ ${project.projectPath}
215
+ `);
216
+ } else if (project.type === 'excel' || project.type === 'excel-macro') {
217
+ console.log(`
218
+ ⚠ Los archivos Excel no se pueden modificar automáticamente.
219
+
220
+ La documentación del workbook está en:
221
+ ${path.join(project.projectPath, 'workbook')}
222
+
223
+ Usa esta documentación como referencia para aplicar
224
+ cambios manualmente en Excel.
225
+ `);
226
+ } else {
227
+ console.log(`✗ Tipo de proyecto no soportado para push: ${project.type}`);
228
+ }
229
+ }
230
+
231
+ module.exports = pushCommand;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Tests for Push Command
3
+ * @module commands/push.test
4
+ */
5
+
6
+ const { test, describe } = require('node:test');
7
+ const assert = require('node:assert');
8
+
9
+ describe('Push Command', () => {
10
+ test('module exports a function', () => {
11
+ const pushCommand = require('./push');
12
+ assert.strictEqual(typeof pushCommand, 'function');
13
+ });
14
+ });
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Search Command (xray)
3
+ * =====================
4
+ * Fuzzy search across snippets, skills, and library content.
5
+ *
6
+ * Usage:
7
+ * super xray "query"
8
+ * super xray --category dax "query"
9
+ * super xray --tag performance
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const Fuse = require('fuse.js');
15
+ const tui = require('../utils/tui');
16
+
17
+ // Fuse.js configuration for fuzzy search
18
+ const FUSE_OPTIONS = {
19
+ keys: [
20
+ { name: 'title', weight: 0.4 },
21
+ { name: 'content', weight: 0.3 },
22
+ { name: 'category', weight: 0.2 },
23
+ { name: 'tags', weight: 0.1 },
24
+ ],
25
+ threshold: 0.4,
26
+ ignoreLocation: true,
27
+ includeScore: true,
28
+ includeMatches: true,
29
+ };
30
+
31
+ /**
32
+ * Build search index from library content
33
+ * @param {string} libraryDir - Path to library directory
34
+ * @returns {Object[]} Array of searchable items
35
+ */
36
+ function buildSearchIndex(libraryDir) {
37
+ const items = [];
38
+
39
+ if (!fs.existsSync(libraryDir)) {
40
+ return items;
41
+ }
42
+
43
+ // Index snippets
44
+ const snippetsDir = path.join(libraryDir, 'snippets');
45
+ if (fs.existsSync(snippetsDir)) {
46
+ indexDirectory(snippetsDir, items, 'snippet');
47
+ }
48
+
49
+ // Index templates
50
+ const templatesDir = path.join(libraryDir, 'templates');
51
+ if (fs.existsSync(templatesDir)) {
52
+ indexDirectory(templatesDir, items, 'template');
53
+ }
54
+
55
+ // Index examples
56
+ const examplesDir = path.join(libraryDir, 'examples');
57
+ if (fs.existsSync(examplesDir)) {
58
+ indexDirectory(examplesDir, items, 'example');
59
+ }
60
+
61
+ return items;
62
+ }
63
+
64
+ /**
65
+ * Recursively index a directory
66
+ * @param {string} dir - Directory path
67
+ * @param {Object[]} items - Items array to populate
68
+ * @param {string} type - Content type
69
+ */
70
+ function indexDirectory(dir, items, type) {
71
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
72
+
73
+ for (const entry of entries) {
74
+ const fullPath = path.join(dir, entry.name);
75
+
76
+ if (entry.isDirectory()) {
77
+ indexDirectory(fullPath, items, type);
78
+ } else if (entry.name.endsWith('.md') && entry.name !== 'README.md') {
79
+ const item = parseMarkdownFile(fullPath, type);
80
+ if (item) {
81
+ items.push(item);
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Parse a markdown file for search indexing
89
+ * @param {string} filePath - File path
90
+ * @param {string} type - Content type
91
+ * @returns {Object|null} Parsed item or null
92
+ */
93
+ function parseMarkdownFile(filePath, type) {
94
+ try {
95
+ const content = fs.readFileSync(filePath, 'utf8');
96
+ const lines = content.split('\n');
97
+
98
+ // Extract title from first H1 or filename
99
+ let title = path.basename(filePath, '.md');
100
+ const h1Match = content.match(/^#\s+(.+)/m);
101
+ if (h1Match) {
102
+ title = h1Match[1];
103
+ }
104
+
105
+ // Extract category from parent directory
106
+ const pathParts = filePath.split(path.sep);
107
+ const snippetsIndex = pathParts.indexOf('snippets');
108
+ const templatesIndex = pathParts.indexOf('templates');
109
+ const examplesIndex = pathParts.indexOf('examples');
110
+
111
+ let category = '';
112
+ const typeIndex = Math.max(snippetsIndex, templatesIndex, examplesIndex);
113
+ if (typeIndex !== -1 && typeIndex + 1 < pathParts.length) {
114
+ category = pathParts[typeIndex + 1];
115
+ }
116
+
117
+ // Extract tags from content (look for patterns like `tag`, **tag**, or explicit tags)
118
+ const tags = [];
119
+ const tagMatches = content.match(/`([^`]+)`/g);
120
+ if (tagMatches) {
121
+ tagMatches.slice(0, 10).forEach((match) => {
122
+ const tag = match.replace(/`/g, '').toLowerCase();
123
+ if (tag.length > 2 && tag.length < 30 && !tags.includes(tag)) {
124
+ tags.push(tag);
125
+ }
126
+ });
127
+ }
128
+
129
+ // Extract first paragraph as preview
130
+ let preview = '';
131
+ for (let i = 0; i < lines.length && i < 20; i++) {
132
+ const line = lines[i].trim();
133
+ if (line && !line.startsWith('#') && !line.startsWith('```') && !line.startsWith('|')) {
134
+ preview = line;
135
+ break;
136
+ }
137
+ }
138
+
139
+ return {
140
+ title,
141
+ path: filePath,
142
+ relativePath: filePath.split('library')[1] || filePath,
143
+ content: content.substring(0, 2000), // Index first 2000 chars
144
+ category,
145
+ type,
146
+ tags,
147
+ preview,
148
+ };
149
+ } catch (e) {
150
+ return null;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Filter results by category
156
+ * @param {Object[]} results - Search results
157
+ * @param {string} category - Category filter
158
+ * @returns {Object[]} Filtered results
159
+ */
160
+ function filterByCategory(results, category) {
161
+ const lowerCategory = category.toLowerCase();
162
+ return results.filter(
163
+ (r) =>
164
+ r.item.category.toLowerCase().includes(lowerCategory) ||
165
+ r.item.type.toLowerCase().includes(lowerCategory)
166
+ );
167
+ }
168
+
169
+ /**
170
+ * Filter results by tag
171
+ * @param {Object[]} results - Search results
172
+ * @param {string} tag - Tag filter
173
+ * @returns {Object[]} Filtered results
174
+ */
175
+ function filterByTag(results, tag) {
176
+ const lowerTag = tag.toLowerCase();
177
+ return results.filter(
178
+ (r) =>
179
+ r.item.tags.some((t) => t.toLowerCase().includes(lowerTag)) ||
180
+ r.item.content.toLowerCase().includes(lowerTag)
181
+ );
182
+ }
183
+
184
+ /**
185
+ * Display search results
186
+ * @param {Object[]} results - Search results from Fuse.js
187
+ * @param {string} query - Original search query
188
+ */
189
+ function displayResults(results, query) {
190
+ if (results.length === 0) {
191
+ tui.warning(`No results found for "${query}"`);
192
+ console.log(
193
+ tui.colors.muted(
194
+ '\nTry a different search term or check available content with: super powers'
195
+ )
196
+ );
197
+ return;
198
+ }
199
+
200
+ tui.section(`Found ${results.length} result${results.length > 1 ? 's' : ''} for "${query}"`);
201
+
202
+ // Group by category
203
+ const grouped = {};
204
+ results.forEach((result) => {
205
+ const cat = result.item.category || result.item.type || 'other';
206
+ if (!grouped[cat]) {
207
+ grouped[cat] = [];
208
+ }
209
+ grouped[cat].push(result);
210
+ });
211
+
212
+ // Display grouped results
213
+ Object.entries(grouped).forEach(([category, categoryResults]) => {
214
+ console.log(`\n${tui.colors.primary(category.toUpperCase())}`);
215
+
216
+ categoryResults.slice(0, 5).forEach((result) => {
217
+ const score = (1 - result.score) * 100;
218
+ const scoreColor =
219
+ score > 70 ? tui.colors.success : score > 40 ? tui.colors.warning : tui.colors.muted;
220
+
221
+ console.log(` ${tui.icons.bullet} ${tui.colors.highlight(result.item.title)}`);
222
+ console.log(` ${tui.colors.muted('Path:')} ${tui.formatPath(result.item.relativePath)}`);
223
+
224
+ if (result.item.preview) {
225
+ console.log(` ${tui.colors.muted(tui.truncate(result.item.preview, 80))}`);
226
+ }
227
+
228
+ console.log(` ${tui.colors.muted('Match:')} ${scoreColor(score.toFixed(0) + '%')}`);
229
+ });
230
+
231
+ if (categoryResults.length > 5) {
232
+ console.log(tui.colors.muted(` ... and ${categoryResults.length - 5} more`));
233
+ }
234
+ });
235
+
236
+ console.log('');
237
+ }
238
+
239
+ /**
240
+ * Parse command line arguments
241
+ * @param {string[]} args - CLI arguments
242
+ * @returns {Object} Parsed options
243
+ */
244
+ function parseArgs(args) {
245
+ const options = {
246
+ query: '',
247
+ category: null,
248
+ tag: null,
249
+ limit: 20,
250
+ };
251
+
252
+ let i = 0;
253
+ while (i < args.length) {
254
+ const arg = args[i];
255
+
256
+ if (arg === '--category' || arg === '-c') {
257
+ options.category = args[++i];
258
+ } else if (arg === '--tag' || arg === '-t') {
259
+ options.tag = args[++i];
260
+ } else if (arg === '--limit' || arg === '-l') {
261
+ options.limit = parseInt(args[++i], 10) || 20;
262
+ } else if (!arg.startsWith('-')) {
263
+ options.query = arg;
264
+ }
265
+ i++;
266
+ }
267
+
268
+ return options;
269
+ }
270
+
271
+ /**
272
+ * Main search command handler
273
+ * @param {string[]} args - Command arguments
274
+ * @param {Object} config - CLI configuration with paths
275
+ */
276
+ function searchCommand(args, config) {
277
+ const options = parseArgs(args);
278
+
279
+ if (!options.query && !options.category && !options.tag) {
280
+ tui.error('Please provide a search query');
281
+ console.log('\nUsage:');
282
+ console.log(' super xray "query" Search for a term');
283
+ console.log(' super xray --category dax Filter by category');
284
+ console.log(' super xray --tag performance Filter by tag');
285
+ console.log('\nExamples:');
286
+ console.log(' super xray "YTD"');
287
+ console.log(' super xray --category dax "time intelligence"');
288
+ console.log(' super xray --tag performance');
289
+ process.exit(1);
290
+ }
291
+
292
+ // Try cache directory first, fall back to local library
293
+ let libraryDir = config.libraryDir;
294
+ const localLibrary = path.join(config.packageDir, 'library');
295
+
296
+ if (!fs.existsSync(libraryDir)) {
297
+ if (fs.existsSync(localLibrary)) {
298
+ libraryDir = localLibrary;
299
+ } else {
300
+ tui.error('Library not found. Run "bi-superpowers unlock" first.');
301
+ process.exit(1);
302
+ }
303
+ }
304
+
305
+ tui.header('BI Agent Superpowers', 'Search Library');
306
+
307
+ // Build search index
308
+ const items = buildSearchIndex(libraryDir);
309
+
310
+ if (items.length === 0) {
311
+ tui.warning('No content found in library');
312
+ return;
313
+ }
314
+
315
+ tui.info(`Indexed ${items.length} items`);
316
+
317
+ // Perform search
318
+ const fuse = new Fuse(items, FUSE_OPTIONS);
319
+
320
+ let results;
321
+ if (options.query) {
322
+ results = fuse.search(options.query);
323
+ } else {
324
+ // If no query, return all items as "results"
325
+ results = items.map((item) => ({ item, score: 0 }));
326
+ }
327
+
328
+ // Apply filters
329
+ if (options.category) {
330
+ results = filterByCategory(results, options.category);
331
+ }
332
+
333
+ if (options.tag) {
334
+ results = filterByTag(results, options.tag);
335
+ }
336
+
337
+ // Limit results
338
+ results = results.slice(0, options.limit);
339
+
340
+ // Display results
341
+ displayResults(results, options.query || `category:${options.category}` || `tag:${options.tag}`);
342
+ }
343
+
344
+ module.exports = searchCommand;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Unit tests for the search command (xray)
3
+ *
4
+ * Run with: npm test
5
+ */
6
+
7
+ const { test, describe } = require('node:test');
8
+ const assert = require('node:assert');
9
+
10
+ // Mock content for testing search functionality
11
+ const mockContent = `# Time Intelligence Patterns
12
+
13
+ ## Overview
14
+ DAX time intelligence functions for YTD, MTD, QTD calculations.
15
+
16
+ ## YTD Pattern
17
+
18
+ \`\`\`dax
19
+ Sales YTD = TOTALYTD([Total Sales], 'Date'[Date])
20
+ \`\`\`
21
+
22
+ ## Rolling Average
23
+
24
+ \`\`\`dax
25
+ Rolling 12M Average =
26
+ AVERAGEX(
27
+ DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -12, MONTH),
28
+ [Total Sales]
29
+ )
30
+ \`\`\`
31
+ `;
32
+
33
+ describe('Search Index Building', () => {
34
+ test('should extract title from H1 heading', () => {
35
+ const h1Match = mockContent.match(/^#\s+(.+)/m);
36
+ assert.ok(h1Match, 'Should find H1 heading');
37
+ assert.strictEqual(h1Match[1], 'Time Intelligence Patterns');
38
+ });
39
+
40
+ test('should extract tags from backtick code', () => {
41
+ const tagMatches = mockContent.match(/`([^`]+)`/g);
42
+ assert.ok(tagMatches, 'Should find backtick tags');
43
+ assert.ok(tagMatches.length > 0, 'Should have at least one tag');
44
+ });
45
+
46
+ test('should find DAX code blocks', () => {
47
+ const daxBlocks = mockContent.match(/```dax[\s\S]*?```/g);
48
+ assert.ok(daxBlocks, 'Should find DAX code blocks');
49
+ assert.strictEqual(daxBlocks.length, 2, 'Should have 2 DAX code blocks');
50
+ });
51
+ });
52
+
53
+ describe('Search Filtering', () => {
54
+ test('category filter should be case-insensitive', () => {
55
+ const category = 'dax';
56
+ const testCategories = ['DAX', 'dax', 'Dax'];
57
+
58
+ testCategories.forEach((testCat) => {
59
+ const matches = testCat.toLowerCase().includes(category.toLowerCase());
60
+ assert.strictEqual(matches, true, `${testCat} should match ${category}`);
61
+ });
62
+ });
63
+
64
+ test('tag filter should find partial matches', () => {
65
+ const tags = ['ytd', 'rolling', 'average', 'time-intelligence'];
66
+ const searchTag = 'roll';
67
+
68
+ const found = tags.some((t) => t.toLowerCase().includes(searchTag.toLowerCase()));
69
+ assert.strictEqual(found, true, 'Should find partial tag match');
70
+ });
71
+ });
72
+
73
+ describe('Search Result Scoring', () => {
74
+ test('perfect match should score higher than partial', () => {
75
+ // Simulating Fuse.js scoring (lower is better, 0 is perfect)
76
+ const perfectScore = 0;
77
+ const partialScore = 0.3;
78
+
79
+ assert.ok(perfectScore < partialScore, 'Perfect match should have lower score');
80
+ });
81
+
82
+ test('title match should have higher weight', () => {
83
+ const titleWeight = 0.4;
84
+ const contentWeight = 0.3;
85
+ const categoryWeight = 0.2;
86
+ const tagsWeight = 0.1;
87
+
88
+ const totalWeight = titleWeight + contentWeight + categoryWeight + tagsWeight;
89
+ // Use tolerance for floating point comparison
90
+ assert.ok(Math.abs(totalWeight - 1.0) < 0.0001, 'Weights should sum to 1.0');
91
+ assert.ok(titleWeight > contentWeight, 'Title should have highest weight');
92
+ });
93
+ });
94
+
95
+ describe('Argument Parsing', () => {
96
+ test('should parse query argument', () => {
97
+ const args = ['YTD'];
98
+ const query = args.find((arg) => !arg.startsWith('-'));
99
+ assert.strictEqual(query, 'YTD');
100
+ });
101
+
102
+ test('should parse category flag', () => {
103
+ const args = ['--category', 'dax', 'YTD'];
104
+ const categoryIndex = args.indexOf('--category');
105
+ const category = categoryIndex !== -1 ? args[categoryIndex + 1] : null;
106
+ assert.strictEqual(category, 'dax');
107
+ });
108
+
109
+ test('should parse short category flag', () => {
110
+ const args = ['-c', 'power-query', 'transform'];
111
+ const categoryIndex = args.indexOf('-c');
112
+ const category = categoryIndex !== -1 ? args[categoryIndex + 1] : null;
113
+ assert.strictEqual(category, 'power-query');
114
+ });
115
+ });