@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,55 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const AdmZip = require('adm-zip');
|
|
7
|
+
const storage = require('./storage');
|
|
8
|
+
|
|
9
|
+
function makeTempDir() {
|
|
10
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'bi-superpowers-'));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test('extractZip extracts safe entries', async () => {
|
|
14
|
+
const dir = makeTempDir();
|
|
15
|
+
const zipPath = path.join(dir, 'safe.zip');
|
|
16
|
+
const zip = new AdmZip();
|
|
17
|
+
zip.addFile('foo.txt', Buffer.from('ok'));
|
|
18
|
+
zip.writeZip(zipPath);
|
|
19
|
+
|
|
20
|
+
const dest = path.join(dir, 'out');
|
|
21
|
+
await storage.extractZip(zipPath, dest);
|
|
22
|
+
|
|
23
|
+
const content = fs.readFileSync(path.join(dest, 'foo.txt'), 'utf8');
|
|
24
|
+
assert.equal(content, 'ok');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('rejects path traversal entries', () => {
|
|
28
|
+
const destRoot = path.resolve(makeTempDir());
|
|
29
|
+
assert.equal(storage._isUnsafeZipEntry('../evil.txt', destRoot), true);
|
|
30
|
+
assert.equal(storage._isUnsafeZipEntry('..\\\\evil.txt', destRoot), true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('rejects absolute path entries', () => {
|
|
34
|
+
const destRoot = path.resolve(makeTempDir());
|
|
35
|
+
assert.equal(storage._isUnsafeZipEntry('/abs.txt', destRoot), true);
|
|
36
|
+
assert.equal(storage._isUnsafeZipEntry('C:\\\\abs.txt', destRoot), true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('downloadFile rejects non-https URLs', async () => {
|
|
40
|
+
const dir = makeTempDir();
|
|
41
|
+
const dest = path.join(dir, 'file.zip');
|
|
42
|
+
|
|
43
|
+
await assert.rejects(
|
|
44
|
+
() => storage.downloadFile('http://example.com/file.zip', dest),
|
|
45
|
+
/HTTPS required/
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('_resolveRedirectUrl resolves relative redirects', () => {
|
|
50
|
+
const resolved = storage._resolveRedirectUrl(
|
|
51
|
+
'https://example.com/path/file.zip',
|
|
52
|
+
'/downloads/content.zip'
|
|
53
|
+
);
|
|
54
|
+
assert.equal(resolved, 'https://example.com/downloads/content.zip');
|
|
55
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License Validator Module
|
|
3
|
+
* ========================
|
|
4
|
+
*
|
|
5
|
+
* Handles license validation against the Acadevor API server.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/licensing/validator
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const https = require('https');
|
|
11
|
+
const http = require('http');
|
|
12
|
+
|
|
13
|
+
const storage = require('./storage');
|
|
14
|
+
|
|
15
|
+
/** Base URL for license validation and content download API */
|
|
16
|
+
const API_BASE_URL = process.env.BIAS_API_URL || 'https://acadevor.com';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Make HTTP(S) request with JSON body
|
|
20
|
+
*
|
|
21
|
+
* Generic HTTP client that:
|
|
22
|
+
* - Automatically selects http/https based on URL protocol
|
|
23
|
+
* - Handles JSON serialization/deserialization
|
|
24
|
+
* - Returns both status code and parsed data
|
|
25
|
+
*
|
|
26
|
+
* @param {string} url - Full URL to request
|
|
27
|
+
* @param {Object} options - Node.js http/https request options
|
|
28
|
+
* @param {string} [options.method='GET'] - HTTP method
|
|
29
|
+
* @param {Object} [options.headers] - Request headers
|
|
30
|
+
* @param {Object} [data] - Request body (will be JSON.stringify'd)
|
|
31
|
+
* @returns {Promise<{status: number, data: Object|string}>} Response object
|
|
32
|
+
* @throws {Error} On network errors
|
|
33
|
+
*/
|
|
34
|
+
function makeRequest(url, options, data) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const urlObj = new URL(url);
|
|
37
|
+
const protocol = urlObj.protocol === 'https:' ? https : http;
|
|
38
|
+
|
|
39
|
+
const req = protocol.request(url, options, (res) => {
|
|
40
|
+
let body = '';
|
|
41
|
+
res.on('data', (chunk) => (body += chunk));
|
|
42
|
+
res.on('end', () => {
|
|
43
|
+
try {
|
|
44
|
+
resolve({
|
|
45
|
+
status: res.statusCode,
|
|
46
|
+
data: body ? JSON.parse(body) : null,
|
|
47
|
+
});
|
|
48
|
+
} catch (e) {
|
|
49
|
+
resolve({
|
|
50
|
+
status: res.statusCode,
|
|
51
|
+
data: body,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
req.on('error', reject);
|
|
58
|
+
|
|
59
|
+
if (data) {
|
|
60
|
+
req.write(JSON.stringify(data));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
req.end();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate license with the Acadevor API server
|
|
69
|
+
*
|
|
70
|
+
* Makes a POST request to /api/licenses/validate to check if a license key
|
|
71
|
+
* is valid and active. Returns license metadata and download URL if valid.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} licenseKey - The license key to validate
|
|
74
|
+
* @returns {Promise<Object>} Validation result
|
|
75
|
+
* @returns {boolean} .valid - Whether the license is valid
|
|
76
|
+
* @returns {string} [.email] - User's email if valid
|
|
77
|
+
* @returns {string} [.name] - User's name if valid
|
|
78
|
+
* @returns {string} [.downloadUrl] - URL to download premium content
|
|
79
|
+
* @returns {string} [.error] - Error message if invalid
|
|
80
|
+
*/
|
|
81
|
+
async function validateLicense(licenseKey) {
|
|
82
|
+
// Skip validation in dev mode (requires secret env var)
|
|
83
|
+
if (process.env.BIAS_DEV_MODE === 'true') {
|
|
84
|
+
return { valid: true, email: 'developer@local' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const response = await makeRequest(
|
|
89
|
+
`${API_BASE_URL}/api/licenses/validate`,
|
|
90
|
+
{
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
},
|
|
94
|
+
{ license: licenseKey }
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return response.data;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('Validation error:', error.message);
|
|
100
|
+
return { valid: false, error: 'Connection failed' };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if license is valid before running premium commands
|
|
106
|
+
*
|
|
107
|
+
* This function acts as a gatekeeper for commands that require premium content.
|
|
108
|
+
* It performs the following checks:
|
|
109
|
+
* 1. Verifies a local license file exists
|
|
110
|
+
* 2. Re-validates the license against the API server
|
|
111
|
+
* 3. Downloads content if not already installed
|
|
112
|
+
*
|
|
113
|
+
* Called by kickoff, recharge, and other premium commands.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} version - Current package version for content versioning
|
|
116
|
+
* @returns {Promise<Object>} Validation result from API
|
|
117
|
+
* @throws {Error} Exits process if license is invalid or content download fails
|
|
118
|
+
*/
|
|
119
|
+
async function requireLicense(version) {
|
|
120
|
+
const license = storage.loadLicense();
|
|
121
|
+
|
|
122
|
+
if (!license) {
|
|
123
|
+
console.log(`
|
|
124
|
+
═══════════════════════════════════════════════════════════════
|
|
125
|
+
LICENSE REQUIRED
|
|
126
|
+
═══════════════════════════════════════════════════════════════
|
|
127
|
+
|
|
128
|
+
BI Agent Superpowers requires a valid license to use.
|
|
129
|
+
|
|
130
|
+
To activate your license, run:
|
|
131
|
+
|
|
132
|
+
super unlock
|
|
133
|
+
|
|
134
|
+
Get your license at: https://acadevor.com/bi-superpowers
|
|
135
|
+
|
|
136
|
+
═══════════════════════════════════════════════════════════════
|
|
137
|
+
`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Re-validate license with server
|
|
142
|
+
console.log('Verifying license...');
|
|
143
|
+
const result = await validateLicense(license.license);
|
|
144
|
+
|
|
145
|
+
if (!result.valid) {
|
|
146
|
+
console.log(`\n✗ License no longer valid: ${result.error}`);
|
|
147
|
+
storage.clearLicense();
|
|
148
|
+
console.log('\nPlease run "bi-superpowers unlock" with a valid license key.');
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(`✓ License valid (${license.email})`);
|
|
153
|
+
|
|
154
|
+
const contentInstalled = storage.isContentInstalled();
|
|
155
|
+
|
|
156
|
+
// Check if content is installed, download if needed
|
|
157
|
+
if (!contentInstalled) {
|
|
158
|
+
console.log('\nPremium content not found. Downloading...');
|
|
159
|
+
if (result.downloadUrl) {
|
|
160
|
+
const downloaded = await storage.downloadPremiumContent(result.downloadUrl);
|
|
161
|
+
if (!downloaded) {
|
|
162
|
+
console.log('\n✗ Content download failed. Please check your internet connection.');
|
|
163
|
+
console.log('You can retry with "bi-superpowers unlock"');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
// Persist the content version once we successfully downloaded the content.
|
|
167
|
+
storage.saveLicense({ ...license, contentVersion: version });
|
|
168
|
+
} else {
|
|
169
|
+
console.log('\n✗ No download URL provided. Please re-activate your license.');
|
|
170
|
+
console.log('Run "bi-superpowers unlock" to download content.');
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
} else if (license.contentVersion !== version) {
|
|
174
|
+
// Best-effort refresh: do not block if the user already has content installed.
|
|
175
|
+
const previousVersion = license.contentVersion || 'unknown';
|
|
176
|
+
console.log(
|
|
177
|
+
`\nPremium content may be outdated (${previousVersion} → ${version}). Refreshing...`
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (result.downloadUrl) {
|
|
181
|
+
const refreshed = await storage.downloadPremiumContent(result.downloadUrl);
|
|
182
|
+
if (refreshed) {
|
|
183
|
+
storage.saveLicense({ ...license, contentVersion: version });
|
|
184
|
+
console.log('✓ Premium content refreshed');
|
|
185
|
+
} else {
|
|
186
|
+
console.log('⚠ Premium content refresh failed. Continuing with existing content.');
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
console.log(
|
|
190
|
+
'⚠ No download URL provided to refresh premium content. Continuing with existing content.'
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log('');
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get the API base URL
|
|
201
|
+
* @returns {string} API base URL
|
|
202
|
+
*/
|
|
203
|
+
function getApiBaseUrl() {
|
|
204
|
+
return API_BASE_URL;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
makeRequest,
|
|
209
|
+
validateLicense,
|
|
210
|
+
requireLicense,
|
|
211
|
+
getApiBaseUrl,
|
|
212
|
+
API_BASE_URL,
|
|
213
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
|
|
5
|
+
const storage = require('./storage');
|
|
6
|
+
|
|
7
|
+
function startLicenseServer() {
|
|
8
|
+
const server = http.createServer((req, res) => {
|
|
9
|
+
if (req.method === 'POST' && req.url === '/api/licenses/validate') {
|
|
10
|
+
let body = '';
|
|
11
|
+
req.on('data', (chunk) => (body += chunk));
|
|
12
|
+
req.on('end', () => {
|
|
13
|
+
// Minimal happy-path response for requireLicense()
|
|
14
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
15
|
+
res.end(
|
|
16
|
+
JSON.stringify({
|
|
17
|
+
valid: true,
|
|
18
|
+
email: 'server@example.com',
|
|
19
|
+
downloadUrl: 'https://example.com/content.zip',
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
27
|
+
res.end(JSON.stringify({ error: 'not found' }));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
server.listen(0, '127.0.0.1', () => {
|
|
32
|
+
const address = server.address();
|
|
33
|
+
resolve({
|
|
34
|
+
server,
|
|
35
|
+
baseUrl: `http://127.0.0.1:${address.port}`,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function patch(obj, key, value) {
|
|
42
|
+
const original = obj[key];
|
|
43
|
+
obj[key] = value;
|
|
44
|
+
return () => {
|
|
45
|
+
obj[key] = original;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('requireLicense refreshes premium content on version mismatch (best-effort)', async () => {
|
|
50
|
+
const { server, baseUrl } = await startLicenseServer();
|
|
51
|
+
const originalDevMode = process.env.BIAS_DEV_MODE;
|
|
52
|
+
process.env.BIAS_API_URL = baseUrl;
|
|
53
|
+
process.env.BIAS_DEV_MODE = 'false';
|
|
54
|
+
delete require.cache[require.resolve('./validator')];
|
|
55
|
+
const validator = require('./validator');
|
|
56
|
+
|
|
57
|
+
const license = {
|
|
58
|
+
license: 'LICENSE_KEY',
|
|
59
|
+
email: 'user@example.com',
|
|
60
|
+
contentVersion: '1.0.0',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let downloadCalls = 0;
|
|
64
|
+
let savedLicense = null;
|
|
65
|
+
|
|
66
|
+
const restores = [
|
|
67
|
+
patch(storage, 'loadLicense', () => license),
|
|
68
|
+
patch(storage, 'isContentInstalled', () => true),
|
|
69
|
+
patch(storage, 'downloadPremiumContent', async () => {
|
|
70
|
+
downloadCalls += 1;
|
|
71
|
+
return true;
|
|
72
|
+
}),
|
|
73
|
+
patch(storage, 'saveLicense', (data) => {
|
|
74
|
+
savedLicense = data;
|
|
75
|
+
}),
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await validator.requireLicense('2.0.0');
|
|
80
|
+
assert.equal(result.valid, true);
|
|
81
|
+
assert.equal(downloadCalls, 1);
|
|
82
|
+
assert.equal(savedLicense.contentVersion, '2.0.0');
|
|
83
|
+
} finally {
|
|
84
|
+
restores.forEach((fn) => fn());
|
|
85
|
+
server.close();
|
|
86
|
+
if (originalDevMode === undefined) {
|
|
87
|
+
delete process.env.BIAS_DEV_MODE;
|
|
88
|
+
} else {
|
|
89
|
+
process.env.BIAS_DEV_MODE = originalDevMode;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('requireLicense does not block when refresh fails but content is installed', async () => {
|
|
95
|
+
const { server, baseUrl } = await startLicenseServer();
|
|
96
|
+
const originalDevMode = process.env.BIAS_DEV_MODE;
|
|
97
|
+
process.env.BIAS_API_URL = baseUrl;
|
|
98
|
+
process.env.BIAS_DEV_MODE = 'false';
|
|
99
|
+
delete require.cache[require.resolve('./validator')];
|
|
100
|
+
const validator = require('./validator');
|
|
101
|
+
|
|
102
|
+
const license = {
|
|
103
|
+
license: 'LICENSE_KEY',
|
|
104
|
+
email: 'user@example.com',
|
|
105
|
+
contentVersion: '1.0.0',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let downloadCalls = 0;
|
|
109
|
+
let saveCalls = 0;
|
|
110
|
+
|
|
111
|
+
const restores = [
|
|
112
|
+
patch(storage, 'loadLicense', () => license),
|
|
113
|
+
patch(storage, 'isContentInstalled', () => true),
|
|
114
|
+
patch(storage, 'downloadPremiumContent', async () => {
|
|
115
|
+
downloadCalls += 1;
|
|
116
|
+
return false;
|
|
117
|
+
}),
|
|
118
|
+
patch(storage, 'saveLicense', () => {
|
|
119
|
+
saveCalls += 1;
|
|
120
|
+
}),
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const result = await validator.requireLicense('2.0.0');
|
|
125
|
+
assert.equal(result.valid, true);
|
|
126
|
+
assert.equal(downloadCalls, 1);
|
|
127
|
+
assert.equal(saveCalls, 0);
|
|
128
|
+
} finally {
|
|
129
|
+
restores.forEach((fn) => fn());
|
|
130
|
+
server.close();
|
|
131
|
+
if (originalDevMode === undefined) {
|
|
132
|
+
delete process.env.BIAS_DEV_MODE;
|
|
133
|
+
} else {
|
|
134
|
+
process.env.BIAS_DEV_MODE = originalDevMode;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official Microsoft MCP Configuration Helpers
|
|
3
|
+
* ============================================
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth for the Claude Code plugin and legacy adapter
|
|
6
|
+
* configurations that point at the official Microsoft Power BI / Fabric
|
|
7
|
+
* MCP servers.
|
|
8
|
+
*
|
|
9
|
+
* @module lib/microsoft-mcp
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const REMOTE_POWERBI_URL = 'https://api.fabric.microsoft.com/v1/mcp/powerbi';
|
|
15
|
+
const FABRIC_MCP_PACKAGE = '@microsoft/fabric-mcp@latest';
|
|
16
|
+
const MODELING_SERVER_NAME = 'powerbi-modeling-mcp';
|
|
17
|
+
const REMOTE_SERVER_NAME = 'powerbi-remote';
|
|
18
|
+
const FABRIC_SERVER_NAME = 'fabric-mcp-server';
|
|
19
|
+
const ABSOLUTE_LAUNCHER_MODE = 'absolute';
|
|
20
|
+
const PLUGIN_ROOT_LAUNCHER_MODE = 'plugin-root';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the launcher script path used to start the official Modeling MCP.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} options - Path resolution options
|
|
26
|
+
* @param {string} [options.packageDir] - Installed package directory
|
|
27
|
+
* @param {string} [options.launcherMode] - `absolute` or `plugin-root`
|
|
28
|
+
* @returns {string} Launcher path expression
|
|
29
|
+
*/
|
|
30
|
+
function resolveModelingLauncherPath(options = {}) {
|
|
31
|
+
const { packageDir, launcherMode = ABSOLUTE_LAUNCHER_MODE } = options;
|
|
32
|
+
|
|
33
|
+
if (launcherMode === PLUGIN_ROOT_LAUNCHER_MODE) {
|
|
34
|
+
return '${CLAUDE_PLUGIN_ROOT}/bin/mcp/powerbi-modeling-launcher.js';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return path.join(packageDir || process.cwd(), 'bin', 'mcp', 'powerbi-modeling-launcher.js');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create the canonical flat MCP config used by Claude Code plugins.
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} options - Generation options
|
|
44
|
+
* @param {string} [options.packageDir] - Installed package directory
|
|
45
|
+
* @param {string} [options.launcherMode] - `absolute` or `plugin-root`
|
|
46
|
+
* @returns {Object} Flat MCP server object
|
|
47
|
+
*/
|
|
48
|
+
function createPluginMcpConfig(options = {}) {
|
|
49
|
+
const launcherPath = resolveModelingLauncherPath(options);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
[REMOTE_SERVER_NAME]: {
|
|
53
|
+
type: 'http',
|
|
54
|
+
url: REMOTE_POWERBI_URL,
|
|
55
|
+
},
|
|
56
|
+
[FABRIC_SERVER_NAME]: {
|
|
57
|
+
type: 'stdio',
|
|
58
|
+
command: 'npx',
|
|
59
|
+
args: ['-y', FABRIC_MCP_PACKAGE, 'server', 'start', '--mode', 'all'],
|
|
60
|
+
},
|
|
61
|
+
[MODELING_SERVER_NAME]: {
|
|
62
|
+
type: 'stdio',
|
|
63
|
+
command: 'node',
|
|
64
|
+
args: [launcherPath],
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convert the canonical flat config to the target client format.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} format - Target format identifier
|
|
73
|
+
* @param {Object} options - Generation options
|
|
74
|
+
* @returns {Object} Config object in the target format
|
|
75
|
+
*/
|
|
76
|
+
function createMcpConfigForFormat(format, options = {}) {
|
|
77
|
+
const flatConfig = createPluginMcpConfig(options);
|
|
78
|
+
|
|
79
|
+
switch (format) {
|
|
80
|
+
case 'plugin':
|
|
81
|
+
case 'cursor':
|
|
82
|
+
return flatConfig;
|
|
83
|
+
|
|
84
|
+
case 'claude':
|
|
85
|
+
case 'kilo':
|
|
86
|
+
case 'vscode':
|
|
87
|
+
return { mcpServers: flatConfig };
|
|
88
|
+
|
|
89
|
+
case 'opencode':
|
|
90
|
+
return {
|
|
91
|
+
mcp: Object.fromEntries(
|
|
92
|
+
Object.entries(flatConfig).map(([name, config]) => {
|
|
93
|
+
if (config.type === 'http') {
|
|
94
|
+
return [name, config];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const quotedArgs = (config.args || []).map((arg) =>
|
|
98
|
+
/\s/.test(arg) ? JSON.stringify(arg) : arg
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return [
|
|
102
|
+
name,
|
|
103
|
+
{
|
|
104
|
+
type: 'local',
|
|
105
|
+
command: [config.command, ...quotedArgs].join(' '),
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
})
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
default:
|
|
113
|
+
return { mcpServers: flatConfig };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Merge a generated MCP config into an existing config object.
|
|
119
|
+
*
|
|
120
|
+
* @param {Object} existingConfig - Existing parsed config
|
|
121
|
+
* @param {Object} newConfig - Generated MCP config
|
|
122
|
+
* @param {string} format - Target format identifier
|
|
123
|
+
* @returns {Object} Merged config
|
|
124
|
+
*/
|
|
125
|
+
function mergeMcpConfig(existingConfig, newConfig, format) {
|
|
126
|
+
const existing = existingConfig || {};
|
|
127
|
+
|
|
128
|
+
switch (format) {
|
|
129
|
+
case 'plugin':
|
|
130
|
+
case 'cursor':
|
|
131
|
+
return {
|
|
132
|
+
...existing,
|
|
133
|
+
...newConfig,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
case 'claude':
|
|
137
|
+
case 'kilo':
|
|
138
|
+
case 'vscode':
|
|
139
|
+
return {
|
|
140
|
+
...existing,
|
|
141
|
+
mcpServers: {
|
|
142
|
+
...(existing.mcpServers || {}),
|
|
143
|
+
...(newConfig.mcpServers || {}),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
case 'opencode':
|
|
148
|
+
return {
|
|
149
|
+
...existing,
|
|
150
|
+
mcp: {
|
|
151
|
+
...(existing.mcp || {}),
|
|
152
|
+
...(newConfig.mcp || {}),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
default:
|
|
157
|
+
return {
|
|
158
|
+
...existing,
|
|
159
|
+
...newConfig,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
ABSOLUTE_LAUNCHER_MODE,
|
|
166
|
+
PLUGIN_ROOT_LAUNCHER_MODE,
|
|
167
|
+
REMOTE_POWERBI_URL,
|
|
168
|
+
FABRIC_MCP_PACKAGE,
|
|
169
|
+
MODELING_SERVER_NAME,
|
|
170
|
+
REMOTE_SERVER_NAME,
|
|
171
|
+
FABRIC_SERVER_NAME,
|
|
172
|
+
resolveModelingLauncherPath,
|
|
173
|
+
createPluginMcpConfig,
|
|
174
|
+
createMcpConfigForFormat,
|
|
175
|
+
mergeMcpConfig,
|
|
176
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for official Microsoft MCP config helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { test, describe } = require('node:test');
|
|
6
|
+
const assert = require('node:assert');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
ABSOLUTE_LAUNCHER_MODE,
|
|
11
|
+
PLUGIN_ROOT_LAUNCHER_MODE,
|
|
12
|
+
REMOTE_POWERBI_URL,
|
|
13
|
+
FABRIC_MCP_PACKAGE,
|
|
14
|
+
createPluginMcpConfig,
|
|
15
|
+
createMcpConfigForFormat,
|
|
16
|
+
mergeMcpConfig,
|
|
17
|
+
resolveModelingLauncherPath,
|
|
18
|
+
} = require('./microsoft-mcp');
|
|
19
|
+
|
|
20
|
+
describe('resolveModelingLauncherPath', () => {
|
|
21
|
+
test('uses plugin root placeholder for plugin builds', () => {
|
|
22
|
+
const launcherPath = resolveModelingLauncherPath({
|
|
23
|
+
launcherMode: PLUGIN_ROOT_LAUNCHER_MODE,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
assert.strictEqual(launcherPath, '${CLAUDE_PLUGIN_ROOT}/bin/mcp/powerbi-modeling-launcher.js');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('uses absolute package path for generated project configs', () => {
|
|
30
|
+
const launcherPath = resolveModelingLauncherPath({
|
|
31
|
+
packageDir: '/tmp/bi-superpowers',
|
|
32
|
+
launcherMode: ABSOLUTE_LAUNCHER_MODE,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
assert.strictEqual(
|
|
36
|
+
launcherPath,
|
|
37
|
+
path.join('/tmp/bi-superpowers', 'bin', 'mcp', 'powerbi-modeling-launcher.js')
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('createPluginMcpConfig', () => {
|
|
43
|
+
test('returns the official Microsoft MCP defaults', () => {
|
|
44
|
+
const config = createPluginMcpConfig({
|
|
45
|
+
packageDir: '/tmp/bi-superpowers',
|
|
46
|
+
launcherMode: ABSOLUTE_LAUNCHER_MODE,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.deepStrictEqual(config['powerbi-remote'], {
|
|
50
|
+
type: 'http',
|
|
51
|
+
url: REMOTE_POWERBI_URL,
|
|
52
|
+
});
|
|
53
|
+
assert.deepStrictEqual(config['fabric-mcp-server'], {
|
|
54
|
+
type: 'stdio',
|
|
55
|
+
command: 'npx',
|
|
56
|
+
args: ['-y', FABRIC_MCP_PACKAGE, 'server', 'start', '--mode', 'all'],
|
|
57
|
+
});
|
|
58
|
+
assert.strictEqual(config['powerbi-modeling-mcp'].type, 'stdio');
|
|
59
|
+
assert.strictEqual(config['powerbi-modeling-mcp'].command, 'node');
|
|
60
|
+
assert.ok(config['powerbi-modeling-mcp'].args[0].endsWith('powerbi-modeling-launcher.js'));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('createMcpConfigForFormat', () => {
|
|
65
|
+
test('returns flat plugin config for plugin/cursor outputs', () => {
|
|
66
|
+
const pluginConfig = createMcpConfigForFormat('plugin');
|
|
67
|
+
const cursorConfig = createMcpConfigForFormat('cursor');
|
|
68
|
+
|
|
69
|
+
assert.ok(pluginConfig['powerbi-remote']);
|
|
70
|
+
assert.ok(cursorConfig['powerbi-remote']);
|
|
71
|
+
assert.strictEqual(pluginConfig['powerbi-remote'].type, 'http');
|
|
72
|
+
assert.strictEqual(cursorConfig['powerbi-remote'].type, 'http');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('returns nested mcpServers config for claude-like outputs', () => {
|
|
76
|
+
const config = createMcpConfigForFormat('claude');
|
|
77
|
+
|
|
78
|
+
assert.ok(config.mcpServers);
|
|
79
|
+
assert.ok(config.mcpServers['powerbi-modeling-mcp']);
|
|
80
|
+
assert.ok(config.mcpServers['fabric-mcp-server']);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('mergeMcpConfig', () => {
|
|
85
|
+
test('merges plugin configs without dropping existing entries', () => {
|
|
86
|
+
const merged = mergeMcpConfig(
|
|
87
|
+
{ existing: { type: 'http', url: 'https://example.com' } },
|
|
88
|
+
{ 'powerbi-remote': { type: 'http', url: REMOTE_POWERBI_URL } },
|
|
89
|
+
'plugin'
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
assert.ok(merged.existing);
|
|
93
|
+
assert.ok(merged['powerbi-remote']);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('merges claude configs inside mcpServers', () => {
|
|
97
|
+
const merged = mergeMcpConfig(
|
|
98
|
+
{ mcpServers: { existing: { type: 'http', url: 'https://example.com' } } },
|
|
99
|
+
{ mcpServers: { 'powerbi-remote': { type: 'http', url: REMOTE_POWERBI_URL } } },
|
|
100
|
+
'claude'
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
assert.ok(merged.mcpServers.existing);
|
|
104
|
+
assert.ok(merged.mcpServers['powerbi-remote']);
|
|
105
|
+
});
|
|
106
|
+
});
|