@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,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Power BI File Utilities for BI Agent Superpowers
|
|
3
|
+
* =================================================
|
|
4
|
+
*
|
|
5
|
+
* Handles extraction and manipulation of Power BI files (.pbix, .pbip).
|
|
6
|
+
*
|
|
7
|
+
* Note: .pbix files are ZIP archives containing:
|
|
8
|
+
* - DataModel (binary - Analysis Services database)
|
|
9
|
+
* - Report/Layout (JSON)
|
|
10
|
+
* - Connections (JSON)
|
|
11
|
+
* - Metadata (XML)
|
|
12
|
+
*
|
|
13
|
+
* For TMDL extraction, we rely on Power BI Desktop's "Save as PBIP" feature
|
|
14
|
+
* or the pbi-tools utility. Direct extraction from .pbix is complex.
|
|
15
|
+
*
|
|
16
|
+
* @module utils/pbix
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Supported file types and their handlers
|
|
24
|
+
*/
|
|
25
|
+
const FILE_TYPES = {
|
|
26
|
+
'.pbix': 'power-bi',
|
|
27
|
+
'.pbip': 'power-bi-project',
|
|
28
|
+
'.xlsx': 'excel',
|
|
29
|
+
'.xlsm': 'excel-macro',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detect the type of BI file
|
|
34
|
+
* @param {string} filePath - Path to the file
|
|
35
|
+
* @returns {Object} File info object
|
|
36
|
+
*/
|
|
37
|
+
function detectFileType(filePath) {
|
|
38
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
39
|
+
const type = FILE_TYPES[ext] || 'unknown';
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
path: filePath,
|
|
43
|
+
extension: ext,
|
|
44
|
+
type,
|
|
45
|
+
name: path.basename(filePath, ext),
|
|
46
|
+
exists: fs.existsSync(filePath),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a file is a Power BI project (.pbip)
|
|
52
|
+
* @param {string} filePath - Path to check
|
|
53
|
+
* @returns {boolean} True if it's a PBIP project
|
|
54
|
+
*/
|
|
55
|
+
function isPbipProject(filePath) {
|
|
56
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
57
|
+
return ext === '.pbip';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the TMDL definition folder for a PBIP project
|
|
62
|
+
* @param {string} pbipPath - Path to .pbip file
|
|
63
|
+
* @returns {string|null} Path to definition folder or null
|
|
64
|
+
*/
|
|
65
|
+
function getPbipDefinitionPath(pbipPath) {
|
|
66
|
+
const dir = path.dirname(pbipPath);
|
|
67
|
+
const name = path.basename(pbipPath, '.pbip');
|
|
68
|
+
|
|
69
|
+
// PBIP structure: Project.pbip + Project.SemanticModel/definition/
|
|
70
|
+
const semanticModelDir = path.join(dir, `${name}.SemanticModel`);
|
|
71
|
+
const definitionDir = path.join(semanticModelDir, 'definition');
|
|
72
|
+
|
|
73
|
+
if (fs.existsSync(definitionDir)) {
|
|
74
|
+
return definitionDir;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Alternative structure: definition folder next to .pbip
|
|
78
|
+
const altDefinitionDir = path.join(dir, 'definition');
|
|
79
|
+
if (fs.existsSync(altDefinitionDir)) {
|
|
80
|
+
return altDefinitionDir;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Copy PBIP definition to the repo project folder
|
|
88
|
+
* @param {string} pbipPath - Path to .pbip file
|
|
89
|
+
* @param {string} projectDir - Target project directory in repo
|
|
90
|
+
* @returns {Object} Result with success status and copied files
|
|
91
|
+
*/
|
|
92
|
+
function extractPbipToRepo(pbipPath, projectDir) {
|
|
93
|
+
const result = {
|
|
94
|
+
success: false,
|
|
95
|
+
files: [],
|
|
96
|
+
error: null,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const definitionPath = getPbipDefinitionPath(pbipPath);
|
|
100
|
+
|
|
101
|
+
if (!definitionPath) {
|
|
102
|
+
result.error = 'Could not find TMDL definition folder for PBIP project';
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const targetDir = path.join(projectDir, 'definition');
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Create target directory
|
|
110
|
+
if (!fs.existsSync(targetDir)) {
|
|
111
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Copy all files recursively
|
|
115
|
+
result.files = copyDirectoryRecursive(definitionPath, targetDir);
|
|
116
|
+
result.success = true;
|
|
117
|
+
} catch (e) {
|
|
118
|
+
result.error = e.message;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Copy directory recursively
|
|
126
|
+
* @param {string} src - Source directory
|
|
127
|
+
* @param {string} dest - Destination directory
|
|
128
|
+
* @returns {string[]} List of copied files (relative paths)
|
|
129
|
+
*/
|
|
130
|
+
function copyDirectoryRecursive(src, dest, baseSrc = src) {
|
|
131
|
+
const copied = [];
|
|
132
|
+
|
|
133
|
+
if (!fs.existsSync(dest)) {
|
|
134
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
138
|
+
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
const srcPath = path.join(src, entry.name);
|
|
141
|
+
const destPath = path.join(dest, entry.name);
|
|
142
|
+
|
|
143
|
+
if (entry.isDirectory()) {
|
|
144
|
+
copied.push(...copyDirectoryRecursive(srcPath, destPath, baseSrc));
|
|
145
|
+
} else {
|
|
146
|
+
fs.copyFileSync(srcPath, destPath);
|
|
147
|
+
copied.push(path.relative(baseSrc, srcPath));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return copied;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get file hash for change detection
|
|
156
|
+
* @param {string} filePath - Path to file
|
|
157
|
+
* @returns {string} Hash string
|
|
158
|
+
*/
|
|
159
|
+
function getFileHash(filePath) {
|
|
160
|
+
if (!fs.existsSync(filePath)) {
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const crypto = require('crypto');
|
|
165
|
+
const content = fs.readFileSync(filePath);
|
|
166
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get last modified time of a file
|
|
171
|
+
* @param {string} filePath - Path to file
|
|
172
|
+
* @returns {Date|null} Last modified date or null
|
|
173
|
+
*/
|
|
174
|
+
function getLastModified(filePath) {
|
|
175
|
+
if (!fs.existsSync(filePath)) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const stats = fs.statSync(filePath);
|
|
180
|
+
return stats.mtime;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Compare two directories and find differences
|
|
185
|
+
* @param {string} dir1 - First directory
|
|
186
|
+
* @param {string} dir2 - Second directory
|
|
187
|
+
* @returns {Object} Differences object
|
|
188
|
+
*/
|
|
189
|
+
function compareDirectories(dir1, dir2) {
|
|
190
|
+
const diff = {
|
|
191
|
+
added: [],
|
|
192
|
+
modified: [],
|
|
193
|
+
deleted: [],
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const files1 = getAllFiles(dir1);
|
|
197
|
+
const files2 = getAllFiles(dir2);
|
|
198
|
+
|
|
199
|
+
const set1 = new Set(files1.map((f) => path.relative(dir1, f)));
|
|
200
|
+
const set2 = new Set(files2.map((f) => path.relative(dir2, f)));
|
|
201
|
+
|
|
202
|
+
// Find added (in dir2 but not dir1)
|
|
203
|
+
for (const file of set2) {
|
|
204
|
+
if (!set1.has(file)) {
|
|
205
|
+
diff.added.push(file);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Find deleted (in dir1 but not dir2)
|
|
210
|
+
for (const file of set1) {
|
|
211
|
+
if (!set2.has(file)) {
|
|
212
|
+
diff.deleted.push(file);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Find modified (in both but different)
|
|
217
|
+
for (const file of set1) {
|
|
218
|
+
if (set2.has(file)) {
|
|
219
|
+
const path1 = path.join(dir1, file);
|
|
220
|
+
const path2 = path.join(dir2, file);
|
|
221
|
+
|
|
222
|
+
if (getFileHash(path1) !== getFileHash(path2)) {
|
|
223
|
+
diff.modified.push(file);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return diff;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get all files in a directory recursively
|
|
233
|
+
* @param {string} dir - Directory to scan
|
|
234
|
+
* @returns {string[]} Array of file paths
|
|
235
|
+
*/
|
|
236
|
+
function getAllFiles(dir) {
|
|
237
|
+
const files = [];
|
|
238
|
+
|
|
239
|
+
if (!fs.existsSync(dir)) {
|
|
240
|
+
return files;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
244
|
+
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
const fullPath = path.join(dir, entry.name);
|
|
247
|
+
|
|
248
|
+
if (entry.isDirectory()) {
|
|
249
|
+
files.push(...getAllFiles(fullPath));
|
|
250
|
+
} else {
|
|
251
|
+
files.push(fullPath);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return files;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Create project.json for a new project
|
|
260
|
+
* @param {Object} options - Project options
|
|
261
|
+
* @returns {Object} Project configuration object
|
|
262
|
+
*/
|
|
263
|
+
function createProjectConfig(options) {
|
|
264
|
+
return {
|
|
265
|
+
name: options.name,
|
|
266
|
+
displayName: options.displayName || options.name,
|
|
267
|
+
type: options.type || 'power-bi',
|
|
268
|
+
profile: options.profile || 'default',
|
|
269
|
+
source: {
|
|
270
|
+
path: options.sourcePath,
|
|
271
|
+
lastSync: new Date().toISOString(),
|
|
272
|
+
hash: options.sourcePath ? getFileHash(options.sourcePath) : '',
|
|
273
|
+
},
|
|
274
|
+
created: new Date().toISOString(),
|
|
275
|
+
description: options.description || '',
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Generate a slug from a display name
|
|
281
|
+
* @param {string} name - Display name
|
|
282
|
+
* @returns {string} URL-friendly slug
|
|
283
|
+
*/
|
|
284
|
+
function generateSlug(name) {
|
|
285
|
+
return name
|
|
286
|
+
.toLowerCase()
|
|
287
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
288
|
+
.replace(/^-+|-+$/g, '')
|
|
289
|
+
.substring(0, 50);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
module.exports = {
|
|
293
|
+
FILE_TYPES,
|
|
294
|
+
detectFileType,
|
|
295
|
+
isPbipProject,
|
|
296
|
+
getPbipDefinitionPath,
|
|
297
|
+
extractPbipToRepo,
|
|
298
|
+
copyDirectoryRecursive,
|
|
299
|
+
getFileHash,
|
|
300
|
+
getLastModified,
|
|
301
|
+
compareDirectories,
|
|
302
|
+
getAllFiles,
|
|
303
|
+
createProjectConfig,
|
|
304
|
+
generateSlug,
|
|
305
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Power BI file utilities
|
|
3
|
+
* @module utils/pbix.test
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const test = require('node:test');
|
|
7
|
+
const assert = require('node:assert/strict');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const pbix = require('./pbix');
|
|
13
|
+
|
|
14
|
+
function makeTempDir() {
|
|
15
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'bi-superpowers-pbix-'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test('copyDirectoryRecursive returns relative paths for nested files', () => {
|
|
19
|
+
const dir = makeTempDir();
|
|
20
|
+
|
|
21
|
+
const src = path.join(dir, 'src');
|
|
22
|
+
const dest = path.join(dir, 'dest');
|
|
23
|
+
|
|
24
|
+
fs.mkdirSync(path.join(src, 'subdir'), { recursive: true });
|
|
25
|
+
fs.writeFileSync(path.join(src, 'root.txt'), 'root');
|
|
26
|
+
fs.writeFileSync(path.join(src, 'subdir', 'nested.txt'), 'nested');
|
|
27
|
+
|
|
28
|
+
const copied = pbix.copyDirectoryRecursive(src, dest);
|
|
29
|
+
|
|
30
|
+
// Returned paths should be relative to the source root
|
|
31
|
+
assert.ok(copied.includes('root.txt'));
|
|
32
|
+
assert.ok(copied.includes(path.join('subdir', 'nested.txt')));
|
|
33
|
+
|
|
34
|
+
// Files should exist in destination with the same structure
|
|
35
|
+
assert.equal(fs.readFileSync(path.join(dest, 'root.txt'), 'utf8'), 'root');
|
|
36
|
+
assert.equal(fs.readFileSync(path.join(dest, 'subdir', 'nested.txt'), 'utf8'), 'nested');
|
|
37
|
+
});
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile Management Utilities for BI Agent Superpowers
|
|
3
|
+
* =====================================================
|
|
4
|
+
*
|
|
5
|
+
* Handles profile configuration, inheritance, and management.
|
|
6
|
+
* Profiles allow users to have different sets of snippets for
|
|
7
|
+
* different industries (finance, retail, healthcare, etc.)
|
|
8
|
+
*
|
|
9
|
+
* @module utils/profiles
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
|
|
16
|
+
/** Base directory for BI Superpowers global config */
|
|
17
|
+
const BASE_CONFIG_DIR = path.join(os.homedir(), '.bi-superpowers');
|
|
18
|
+
|
|
19
|
+
/** Path to global config file */
|
|
20
|
+
const CONFIG_FILE = path.join(BASE_CONFIG_DIR, 'config.json');
|
|
21
|
+
|
|
22
|
+
/** Path to profiles directory */
|
|
23
|
+
const PROFILES_DIR = path.join(BASE_CONFIG_DIR, 'profiles');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Default configuration structure
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_CONFIG = {
|
|
29
|
+
version: '3.0',
|
|
30
|
+
defaultProfile: 'default',
|
|
31
|
+
profiles: {},
|
|
32
|
+
repoPath: null,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Ensure the base config directory exists
|
|
37
|
+
*/
|
|
38
|
+
function ensureConfigDir() {
|
|
39
|
+
if (!fs.existsSync(BASE_CONFIG_DIR)) {
|
|
40
|
+
fs.mkdirSync(BASE_CONFIG_DIR, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
if (!fs.existsSync(PROFILES_DIR)) {
|
|
43
|
+
fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load the global configuration
|
|
49
|
+
* @returns {Object} Configuration object
|
|
50
|
+
*/
|
|
51
|
+
function loadConfig() {
|
|
52
|
+
ensureConfigDir();
|
|
53
|
+
|
|
54
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
55
|
+
try {
|
|
56
|
+
const data = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
57
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return { ...DEFAULT_CONFIG };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { ...DEFAULT_CONFIG };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Save the global configuration
|
|
68
|
+
* @param {Object} config - Configuration to save
|
|
69
|
+
*/
|
|
70
|
+
function saveConfig(config) {
|
|
71
|
+
ensureConfigDir();
|
|
72
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the path to the user's bi-repo
|
|
77
|
+
* @returns {string|null} Path to bi-repo or null if not configured
|
|
78
|
+
*/
|
|
79
|
+
function getRepoPath() {
|
|
80
|
+
const config = loadConfig();
|
|
81
|
+
return config.repoPath;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Set the path to the user's bi-repo
|
|
86
|
+
* @param {string} repoPath - Path to the bi-repo
|
|
87
|
+
*/
|
|
88
|
+
function setRepoPath(repoPath) {
|
|
89
|
+
const config = loadConfig();
|
|
90
|
+
config.repoPath = repoPath;
|
|
91
|
+
saveConfig(config);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get list of available profiles
|
|
96
|
+
* @returns {string[]} Array of profile names
|
|
97
|
+
*/
|
|
98
|
+
function listProfiles() {
|
|
99
|
+
ensureConfigDir();
|
|
100
|
+
|
|
101
|
+
if (!fs.existsSync(PROFILES_DIR)) {
|
|
102
|
+
return ['default'];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const profiles = fs
|
|
106
|
+
.readdirSync(PROFILES_DIR)
|
|
107
|
+
.filter((f) => fs.statSync(path.join(PROFILES_DIR, f)).isDirectory());
|
|
108
|
+
|
|
109
|
+
// Always include default
|
|
110
|
+
if (!profiles.includes('default')) {
|
|
111
|
+
profiles.unshift('default');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return profiles;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if a profile exists
|
|
119
|
+
* @param {string} profileName - Name of the profile
|
|
120
|
+
* @returns {boolean} True if profile exists
|
|
121
|
+
*/
|
|
122
|
+
function profileExists(profileName) {
|
|
123
|
+
const profilePath = path.join(PROFILES_DIR, profileName);
|
|
124
|
+
return fs.existsSync(profilePath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create a new profile
|
|
129
|
+
* @param {string} profileName - Name of the new profile
|
|
130
|
+
* @param {Object} options - Options for the profile
|
|
131
|
+
* @param {string} options.inheritsFrom - Parent profile name
|
|
132
|
+
* @returns {boolean} True if successful
|
|
133
|
+
*/
|
|
134
|
+
function createProfile(profileName, options = {}) {
|
|
135
|
+
ensureConfigDir();
|
|
136
|
+
|
|
137
|
+
const profilePath = path.join(PROFILES_DIR, profileName);
|
|
138
|
+
|
|
139
|
+
if (fs.existsSync(profilePath)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create profile directory structure
|
|
144
|
+
fs.mkdirSync(profilePath, { recursive: true });
|
|
145
|
+
fs.mkdirSync(path.join(profilePath, 'snippets'), { recursive: true });
|
|
146
|
+
|
|
147
|
+
// Update config with inheritance
|
|
148
|
+
const config = loadConfig();
|
|
149
|
+
if (options.inheritsFrom) {
|
|
150
|
+
config.profiles[profileName] = { inheritsFrom: options.inheritsFrom };
|
|
151
|
+
} else {
|
|
152
|
+
config.profiles[profileName] = {};
|
|
153
|
+
}
|
|
154
|
+
saveConfig(config);
|
|
155
|
+
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get the inheritance chain for a profile
|
|
161
|
+
* @param {string} profileName - Name of the profile
|
|
162
|
+
* @returns {string[]} Array of profile names from most specific to base
|
|
163
|
+
*/
|
|
164
|
+
function getInheritanceChain(profileName) {
|
|
165
|
+
const config = loadConfig();
|
|
166
|
+
const chain = [profileName];
|
|
167
|
+
|
|
168
|
+
let current = profileName;
|
|
169
|
+
const visited = new Set([profileName]);
|
|
170
|
+
|
|
171
|
+
while (config.profiles[current]?.inheritsFrom) {
|
|
172
|
+
const parent = config.profiles[current].inheritsFrom;
|
|
173
|
+
|
|
174
|
+
// Prevent circular dependencies
|
|
175
|
+
if (visited.has(parent)) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
chain.push(parent);
|
|
180
|
+
visited.add(parent);
|
|
181
|
+
current = parent;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return chain;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get all snippets for a profile, including inherited ones
|
|
189
|
+
* @param {string} profileName - Name of the profile
|
|
190
|
+
* @returns {Object[]} Array of snippet objects with path and profile
|
|
191
|
+
*/
|
|
192
|
+
function getProfileSnippets(profileName) {
|
|
193
|
+
const chain = getInheritanceChain(profileName);
|
|
194
|
+
const snippets = [];
|
|
195
|
+
const seen = new Set();
|
|
196
|
+
|
|
197
|
+
// Process from most specific to least specific
|
|
198
|
+
for (const profile of chain) {
|
|
199
|
+
const profilePath = path.join(PROFILES_DIR, profile, 'snippets');
|
|
200
|
+
|
|
201
|
+
if (!fs.existsSync(profilePath)) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Recursively find all .md files
|
|
206
|
+
const files = findMdFiles(profilePath);
|
|
207
|
+
|
|
208
|
+
for (const file of files) {
|
|
209
|
+
const relativePath = path.relative(profilePath, file);
|
|
210
|
+
|
|
211
|
+
// Skip if we already have this snippet from a more specific profile
|
|
212
|
+
if (!seen.has(relativePath)) {
|
|
213
|
+
seen.add(relativePath);
|
|
214
|
+
snippets.push({
|
|
215
|
+
path: file,
|
|
216
|
+
relativePath,
|
|
217
|
+
profile,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return snippets;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Recursively find all .md files in a directory
|
|
228
|
+
* @param {string} dir - Directory to search
|
|
229
|
+
* @returns {string[]} Array of file paths
|
|
230
|
+
*/
|
|
231
|
+
function findMdFiles(dir) {
|
|
232
|
+
const files = [];
|
|
233
|
+
|
|
234
|
+
if (!fs.existsSync(dir)) {
|
|
235
|
+
return files;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
239
|
+
|
|
240
|
+
for (const entry of entries) {
|
|
241
|
+
const fullPath = path.join(dir, entry.name);
|
|
242
|
+
|
|
243
|
+
if (entry.isDirectory()) {
|
|
244
|
+
files.push(...findMdFiles(fullPath));
|
|
245
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
246
|
+
files.push(fullPath);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return files;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Copy a snippet to a profile
|
|
255
|
+
* @param {string} sourcePath - Path to the source snippet
|
|
256
|
+
* @param {string} profileName - Target profile name
|
|
257
|
+
* @param {string} relativePath - Relative path within snippets folder
|
|
258
|
+
* @returns {boolean} True if successful
|
|
259
|
+
*/
|
|
260
|
+
function copySnippetToProfile(sourcePath, profileName, relativePath) {
|
|
261
|
+
const targetPath = path.join(PROFILES_DIR, profileName, 'snippets', relativePath);
|
|
262
|
+
const targetDir = path.dirname(targetPath);
|
|
263
|
+
|
|
264
|
+
if (!fs.existsSync(targetDir)) {
|
|
265
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
270
|
+
return true;
|
|
271
|
+
} catch (e) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get the default profile name
|
|
278
|
+
* @returns {string} Default profile name
|
|
279
|
+
*/
|
|
280
|
+
function getDefaultProfile() {
|
|
281
|
+
const config = loadConfig();
|
|
282
|
+
return config.defaultProfile || 'default';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Set the default profile
|
|
287
|
+
* @param {string} profileName - Profile to set as default
|
|
288
|
+
*/
|
|
289
|
+
function setDefaultProfile(profileName) {
|
|
290
|
+
const config = loadConfig();
|
|
291
|
+
config.defaultProfile = profileName;
|
|
292
|
+
saveConfig(config);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
module.exports = {
|
|
296
|
+
BASE_CONFIG_DIR,
|
|
297
|
+
PROFILES_DIR,
|
|
298
|
+
ensureConfigDir,
|
|
299
|
+
loadConfig,
|
|
300
|
+
saveConfig,
|
|
301
|
+
getRepoPath,
|
|
302
|
+
setRepoPath,
|
|
303
|
+
listProfiles,
|
|
304
|
+
profileExists,
|
|
305
|
+
createProfile,
|
|
306
|
+
getInheritanceChain,
|
|
307
|
+
getProfileSnippets,
|
|
308
|
+
findMdFiles,
|
|
309
|
+
copySnippetToProfile,
|
|
310
|
+
getDefaultProfile,
|
|
311
|
+
setDefaultProfile,
|
|
312
|
+
};
|