@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.
- package/.claude-plugin/plugin.json +8 -0
- package/.mcp.json +25 -0
- package/AGENTS.md +244 -0
- package/CHANGELOG.md +265 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/bin/build-plugin.js +30 -0
- package/bin/cli.js +1064 -0
- package/bin/commands/add.js +533 -0
- package/bin/commands/add.test.js +77 -0
- package/bin/commands/build-desktop.js +166 -0
- package/bin/commands/changelog.js +443 -0
- package/bin/commands/diff.js +325 -0
- package/bin/commands/lint.js +419 -0
- package/bin/commands/lint.test.js +103 -0
- package/bin/commands/mcp-setup.js +246 -0
- package/bin/commands/pull.js +287 -0
- package/bin/commands/pull.test.js +36 -0
- package/bin/commands/push.js +231 -0
- package/bin/commands/push.test.js +14 -0
- package/bin/commands/search.js +344 -0
- package/bin/commands/search.test.js +115 -0
- package/bin/commands/setup.js +545 -0
- package/bin/commands/setup.test.js +46 -0
- package/bin/commands/sync-profile.js +405 -0
- package/bin/commands/sync-profile.test.js +14 -0
- package/bin/commands/sync-source.js +418 -0
- package/bin/commands/sync-source.test.js +14 -0
- package/bin/commands/watch.js +206 -0
- package/bin/lib/generators/claude-plugin.js +266 -0
- package/bin/lib/generators/claude-plugin.test.js +110 -0
- package/bin/lib/generators/index.js +116 -0
- package/bin/lib/generators/shared.js +282 -0
- package/bin/lib/licensing/index.js +35 -0
- package/bin/lib/licensing/storage.js +364 -0
- package/bin/lib/licensing/storage.test.js +55 -0
- package/bin/lib/licensing/validator.js +213 -0
- package/bin/lib/licensing/validator.test.js +137 -0
- package/bin/lib/microsoft-mcp.js +176 -0
- package/bin/lib/microsoft-mcp.test.js +106 -0
- package/bin/lib/skills.js +84 -0
- package/bin/mcp/powerbi-modeling-launcher.js +38 -0
- package/bin/postinstall.js +44 -0
- package/bin/utils/errors.js +159 -0
- package/bin/utils/git.js +298 -0
- package/bin/utils/logger.js +142 -0
- package/bin/utils/mcp-detect.js +274 -0
- package/bin/utils/mcp-detect.test.js +105 -0
- package/bin/utils/pbix.js +305 -0
- package/bin/utils/pbix.test.js +37 -0
- package/bin/utils/profiles.js +312 -0
- package/bin/utils/projects.js +168 -0
- package/bin/utils/readline.js +206 -0
- package/bin/utils/readline.test.js +47 -0
- package/bin/utils/tui.js +314 -0
- package/bin/utils/tui.test.js +127 -0
- package/commands/contributions.md +265 -0
- package/commands/data-model-design.md +468 -0
- package/commands/dax-doctor.md +248 -0
- package/commands/fabric-scripts.md +452 -0
- package/commands/migration-assistant.md +290 -0
- package/commands/model-documenter.md +242 -0
- package/commands/pbi-connect.md +239 -0
- package/commands/project-kickoff.md +905 -0
- package/commands/report-layout.md +296 -0
- package/commands/rls-design.md +533 -0
- package/commands/theme-tweaker.md +624 -0
- package/config.example.json +23 -0
- package/config.json +23 -0
- package/desktop-extension/manifest.json +37 -0
- package/desktop-extension/package.json +10 -0
- package/desktop-extension/server.js +95 -0
- package/docs/openrouter-free-models.md +92 -0
- package/library/examples/README.md +151 -0
- package/library/examples/finance-reporting/README.md +351 -0
- package/library/examples/finance-reporting/data-model.md +267 -0
- package/library/examples/finance-reporting/measures.dax +557 -0
- package/library/examples/hr-analytics/README.md +371 -0
- package/library/examples/hr-analytics/data-model.md +315 -0
- package/library/examples/hr-analytics/measures.dax +460 -0
- package/library/examples/marketing-analytics/README.md +37 -0
- package/library/examples/marketing-analytics/data-model.md +62 -0
- package/library/examples/marketing-analytics/measures.dax +110 -0
- package/library/examples/retail-analytics/README.md +439 -0
- package/library/examples/retail-analytics/data-model.md +288 -0
- package/library/examples/retail-analytics/measures.dax +481 -0
- package/library/examples/supply-chain/README.md +37 -0
- package/library/examples/supply-chain/data-model.md +69 -0
- package/library/examples/supply-chain/measures.dax +77 -0
- package/library/examples/udf-library/README.md +228 -0
- package/library/examples/udf-library/functions.dax +571 -0
- package/library/snippets/dax/README.md +292 -0
- package/library/snippets/dax/business-domains.md +576 -0
- package/library/snippets/dax/calculate-patterns.md +276 -0
- package/library/snippets/dax/calculation-groups.md +489 -0
- package/library/snippets/dax/error-handling.md +495 -0
- package/library/snippets/dax/iterators-and-aggregations.md +474 -0
- package/library/snippets/dax/kpis-and-metrics.md +293 -0
- package/library/snippets/dax/rankings-and-topn.md +235 -0
- package/library/snippets/dax/security-patterns.md +413 -0
- package/library/snippets/dax/text-and-formatting.md +316 -0
- package/library/snippets/dax/time-intelligence.md +196 -0
- package/library/snippets/dax/user-defined-functions.md +477 -0
- package/library/snippets/dax/virtual-tables.md +546 -0
- package/library/snippets/excel-formulas/README.md +84 -0
- package/library/snippets/excel-formulas/aggregations.md +330 -0
- package/library/snippets/excel-formulas/dates-and-times.md +361 -0
- package/library/snippets/excel-formulas/dynamic-arrays.md +314 -0
- package/library/snippets/excel-formulas/lookups.md +169 -0
- package/library/snippets/excel-formulas/text-functions.md +363 -0
- package/library/snippets/governance/naming-conventions.md +97 -0
- package/library/snippets/governance/review-checklists.md +107 -0
- package/library/snippets/power-query/README.md +389 -0
- package/library/snippets/power-query/api-integration.md +707 -0
- package/library/snippets/power-query/connections.md +434 -0
- package/library/snippets/power-query/data-cleaning.md +298 -0
- package/library/snippets/power-query/error-handling.md +526 -0
- package/library/snippets/power-query/parameters.md +350 -0
- package/library/snippets/power-query/performance.md +506 -0
- package/library/snippets/power-query/transformations.md +330 -0
- package/library/snippets/report-design/accessibility.md +78 -0
- package/library/snippets/report-design/chart-selection.md +54 -0
- package/library/snippets/report-design/layout-patterns.md +87 -0
- package/library/templates/data-models/README.md +93 -0
- package/library/templates/data-models/finance-model.md +627 -0
- package/library/templates/data-models/retail-star-schema.md +473 -0
- package/library/templates/excel/README.md +83 -0
- package/library/templates/excel/budget-tracker.md +432 -0
- package/library/templates/excel/data-entry-form.md +533 -0
- package/library/templates/power-bi/README.md +72 -0
- package/library/templates/power-bi/finance-report.md +449 -0
- package/library/templates/power-bi/kpi-scorecard.md +461 -0
- package/library/templates/power-bi/sales-dashboard.md +281 -0
- package/library/themes/excel/README.md +436 -0
- package/library/themes/power-bi/README.md +271 -0
- package/library/themes/power-bi/accessible.json +307 -0
- package/library/themes/power-bi/bi-superpowers-default.json +858 -0
- package/library/themes/power-bi/corporate-blue.json +291 -0
- package/library/themes/power-bi/dark-mode.json +291 -0
- package/library/themes/power-bi/minimal.json +292 -0
- package/library/themes/power-bi/print-friendly.json +309 -0
- package/package.json +93 -0
- package/skills/contributions/SKILL.md +267 -0
- package/skills/data-model-design/SKILL.md +470 -0
- package/skills/data-modeling/SKILL.md +254 -0
- package/skills/data-quality/SKILL.md +664 -0
- package/skills/dax/SKILL.md +708 -0
- package/skills/dax-doctor/SKILL.md +250 -0
- package/skills/dax-udf/SKILL.md +489 -0
- package/skills/deployment/SKILL.md +320 -0
- package/skills/excel-formulas/SKILL.md +463 -0
- package/skills/fabric-scripts/SKILL.md +454 -0
- package/skills/fast-standard/SKILL.md +509 -0
- package/skills/governance/SKILL.md +205 -0
- package/skills/migration-assistant/SKILL.md +292 -0
- package/skills/model-documenter/SKILL.md +244 -0
- package/skills/pbi-connect/SKILL.md +241 -0
- package/skills/power-query/SKILL.md +406 -0
- package/skills/project-kickoff/SKILL.md +907 -0
- package/skills/query-performance/SKILL.md +480 -0
- package/skills/report-design/SKILL.md +207 -0
- package/skills/report-layout/SKILL.md +298 -0
- package/skills/rls-design/SKILL.md +535 -0
- package/skills/semantic-model/SKILL.md +237 -0
- package/skills/testing-validation/SKILL.md +643 -0
- package/skills/theme-tweaker/SKILL.md +626 -0
- package/src/content/base.md +237 -0
- package/src/content/mcp-requirements.json +69 -0
- package/src/content/routing.md +203 -0
- package/src/content/skills/contributions.md +259 -0
- package/src/content/skills/data-model-design.md +462 -0
- package/src/content/skills/data-modeling.md +246 -0
- package/src/content/skills/data-quality.md +656 -0
- package/src/content/skills/dax-doctor.md +242 -0
- package/src/content/skills/dax-udf.md +481 -0
- package/src/content/skills/dax.md +700 -0
- package/src/content/skills/deployment.md +312 -0
- package/src/content/skills/excel-formulas.md +455 -0
- package/src/content/skills/fabric-scripts.md +446 -0
- package/src/content/skills/fast-standard.md +501 -0
- package/src/content/skills/governance.md +197 -0
- package/src/content/skills/migration-assistant.md +284 -0
- package/src/content/skills/model-documenter.md +236 -0
- package/src/content/skills/pbi-connect.md +233 -0
- package/src/content/skills/power-query.md +398 -0
- package/src/content/skills/project-kickoff.md +899 -0
- package/src/content/skills/query-performance.md +472 -0
- package/src/content/skills/report-design.md +199 -0
- package/src/content/skills/report-layout.md +290 -0
- package/src/content/skills/rls-design.md +527 -0
- package/src/content/skills/semantic-model.md +229 -0
- package/src/content/skills/testing-validation.md +635 -0
- 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
|
+
});
|