@luquimbo/bi-superpowers 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/.claude-plugin/plugin.json +8 -0
  2. package/.mcp.json +25 -0
  3. package/AGENTS.md +244 -0
  4. package/CHANGELOG.md +265 -0
  5. package/LICENSE +21 -0
  6. package/README.md +211 -0
  7. package/bin/build-plugin.js +30 -0
  8. package/bin/cli.js +1064 -0
  9. package/bin/commands/add.js +533 -0
  10. package/bin/commands/add.test.js +77 -0
  11. package/bin/commands/build-desktop.js +166 -0
  12. package/bin/commands/changelog.js +443 -0
  13. package/bin/commands/diff.js +325 -0
  14. package/bin/commands/lint.js +419 -0
  15. package/bin/commands/lint.test.js +103 -0
  16. package/bin/commands/mcp-setup.js +246 -0
  17. package/bin/commands/pull.js +287 -0
  18. package/bin/commands/pull.test.js +36 -0
  19. package/bin/commands/push.js +231 -0
  20. package/bin/commands/push.test.js +14 -0
  21. package/bin/commands/search.js +344 -0
  22. package/bin/commands/search.test.js +115 -0
  23. package/bin/commands/setup.js +545 -0
  24. package/bin/commands/setup.test.js +46 -0
  25. package/bin/commands/sync-profile.js +405 -0
  26. package/bin/commands/sync-profile.test.js +14 -0
  27. package/bin/commands/sync-source.js +418 -0
  28. package/bin/commands/sync-source.test.js +14 -0
  29. package/bin/commands/watch.js +206 -0
  30. package/bin/lib/generators/claude-plugin.js +266 -0
  31. package/bin/lib/generators/claude-plugin.test.js +110 -0
  32. package/bin/lib/generators/index.js +116 -0
  33. package/bin/lib/generators/shared.js +282 -0
  34. package/bin/lib/licensing/index.js +35 -0
  35. package/bin/lib/licensing/storage.js +364 -0
  36. package/bin/lib/licensing/storage.test.js +55 -0
  37. package/bin/lib/licensing/validator.js +213 -0
  38. package/bin/lib/licensing/validator.test.js +137 -0
  39. package/bin/lib/microsoft-mcp.js +176 -0
  40. package/bin/lib/microsoft-mcp.test.js +106 -0
  41. package/bin/lib/skills.js +84 -0
  42. package/bin/mcp/powerbi-modeling-launcher.js +38 -0
  43. package/bin/postinstall.js +44 -0
  44. package/bin/utils/errors.js +159 -0
  45. package/bin/utils/git.js +298 -0
  46. package/bin/utils/logger.js +142 -0
  47. package/bin/utils/mcp-detect.js +274 -0
  48. package/bin/utils/mcp-detect.test.js +105 -0
  49. package/bin/utils/pbix.js +305 -0
  50. package/bin/utils/pbix.test.js +37 -0
  51. package/bin/utils/profiles.js +312 -0
  52. package/bin/utils/projects.js +168 -0
  53. package/bin/utils/readline.js +206 -0
  54. package/bin/utils/readline.test.js +47 -0
  55. package/bin/utils/tui.js +314 -0
  56. package/bin/utils/tui.test.js +127 -0
  57. package/commands/contributions.md +265 -0
  58. package/commands/data-model-design.md +468 -0
  59. package/commands/dax-doctor.md +248 -0
  60. package/commands/fabric-scripts.md +452 -0
  61. package/commands/migration-assistant.md +290 -0
  62. package/commands/model-documenter.md +242 -0
  63. package/commands/pbi-connect.md +239 -0
  64. package/commands/project-kickoff.md +905 -0
  65. package/commands/report-layout.md +296 -0
  66. package/commands/rls-design.md +533 -0
  67. package/commands/theme-tweaker.md +624 -0
  68. package/config.example.json +23 -0
  69. package/config.json +23 -0
  70. package/desktop-extension/manifest.json +37 -0
  71. package/desktop-extension/package.json +10 -0
  72. package/desktop-extension/server.js +95 -0
  73. package/docs/openrouter-free-models.md +92 -0
  74. package/library/examples/README.md +151 -0
  75. package/library/examples/finance-reporting/README.md +351 -0
  76. package/library/examples/finance-reporting/data-model.md +267 -0
  77. package/library/examples/finance-reporting/measures.dax +557 -0
  78. package/library/examples/hr-analytics/README.md +371 -0
  79. package/library/examples/hr-analytics/data-model.md +315 -0
  80. package/library/examples/hr-analytics/measures.dax +460 -0
  81. package/library/examples/marketing-analytics/README.md +37 -0
  82. package/library/examples/marketing-analytics/data-model.md +62 -0
  83. package/library/examples/marketing-analytics/measures.dax +110 -0
  84. package/library/examples/retail-analytics/README.md +439 -0
  85. package/library/examples/retail-analytics/data-model.md +288 -0
  86. package/library/examples/retail-analytics/measures.dax +481 -0
  87. package/library/examples/supply-chain/README.md +37 -0
  88. package/library/examples/supply-chain/data-model.md +69 -0
  89. package/library/examples/supply-chain/measures.dax +77 -0
  90. package/library/examples/udf-library/README.md +228 -0
  91. package/library/examples/udf-library/functions.dax +571 -0
  92. package/library/snippets/dax/README.md +292 -0
  93. package/library/snippets/dax/business-domains.md +576 -0
  94. package/library/snippets/dax/calculate-patterns.md +276 -0
  95. package/library/snippets/dax/calculation-groups.md +489 -0
  96. package/library/snippets/dax/error-handling.md +495 -0
  97. package/library/snippets/dax/iterators-and-aggregations.md +474 -0
  98. package/library/snippets/dax/kpis-and-metrics.md +293 -0
  99. package/library/snippets/dax/rankings-and-topn.md +235 -0
  100. package/library/snippets/dax/security-patterns.md +413 -0
  101. package/library/snippets/dax/text-and-formatting.md +316 -0
  102. package/library/snippets/dax/time-intelligence.md +196 -0
  103. package/library/snippets/dax/user-defined-functions.md +477 -0
  104. package/library/snippets/dax/virtual-tables.md +546 -0
  105. package/library/snippets/excel-formulas/README.md +84 -0
  106. package/library/snippets/excel-formulas/aggregations.md +330 -0
  107. package/library/snippets/excel-formulas/dates-and-times.md +361 -0
  108. package/library/snippets/excel-formulas/dynamic-arrays.md +314 -0
  109. package/library/snippets/excel-formulas/lookups.md +169 -0
  110. package/library/snippets/excel-formulas/text-functions.md +363 -0
  111. package/library/snippets/governance/naming-conventions.md +97 -0
  112. package/library/snippets/governance/review-checklists.md +107 -0
  113. package/library/snippets/power-query/README.md +389 -0
  114. package/library/snippets/power-query/api-integration.md +707 -0
  115. package/library/snippets/power-query/connections.md +434 -0
  116. package/library/snippets/power-query/data-cleaning.md +298 -0
  117. package/library/snippets/power-query/error-handling.md +526 -0
  118. package/library/snippets/power-query/parameters.md +350 -0
  119. package/library/snippets/power-query/performance.md +506 -0
  120. package/library/snippets/power-query/transformations.md +330 -0
  121. package/library/snippets/report-design/accessibility.md +78 -0
  122. package/library/snippets/report-design/chart-selection.md +54 -0
  123. package/library/snippets/report-design/layout-patterns.md +87 -0
  124. package/library/templates/data-models/README.md +93 -0
  125. package/library/templates/data-models/finance-model.md +627 -0
  126. package/library/templates/data-models/retail-star-schema.md +473 -0
  127. package/library/templates/excel/README.md +83 -0
  128. package/library/templates/excel/budget-tracker.md +432 -0
  129. package/library/templates/excel/data-entry-form.md +533 -0
  130. package/library/templates/power-bi/README.md +72 -0
  131. package/library/templates/power-bi/finance-report.md +449 -0
  132. package/library/templates/power-bi/kpi-scorecard.md +461 -0
  133. package/library/templates/power-bi/sales-dashboard.md +281 -0
  134. package/library/themes/excel/README.md +436 -0
  135. package/library/themes/power-bi/README.md +271 -0
  136. package/library/themes/power-bi/accessible.json +307 -0
  137. package/library/themes/power-bi/bi-superpowers-default.json +858 -0
  138. package/library/themes/power-bi/corporate-blue.json +291 -0
  139. package/library/themes/power-bi/dark-mode.json +291 -0
  140. package/library/themes/power-bi/minimal.json +292 -0
  141. package/library/themes/power-bi/print-friendly.json +309 -0
  142. package/package.json +93 -0
  143. package/skills/contributions/SKILL.md +267 -0
  144. package/skills/data-model-design/SKILL.md +470 -0
  145. package/skills/data-modeling/SKILL.md +254 -0
  146. package/skills/data-quality/SKILL.md +664 -0
  147. package/skills/dax/SKILL.md +708 -0
  148. package/skills/dax-doctor/SKILL.md +250 -0
  149. package/skills/dax-udf/SKILL.md +489 -0
  150. package/skills/deployment/SKILL.md +320 -0
  151. package/skills/excel-formulas/SKILL.md +463 -0
  152. package/skills/fabric-scripts/SKILL.md +454 -0
  153. package/skills/fast-standard/SKILL.md +509 -0
  154. package/skills/governance/SKILL.md +205 -0
  155. package/skills/migration-assistant/SKILL.md +292 -0
  156. package/skills/model-documenter/SKILL.md +244 -0
  157. package/skills/pbi-connect/SKILL.md +241 -0
  158. package/skills/power-query/SKILL.md +406 -0
  159. package/skills/project-kickoff/SKILL.md +907 -0
  160. package/skills/query-performance/SKILL.md +480 -0
  161. package/skills/report-design/SKILL.md +207 -0
  162. package/skills/report-layout/SKILL.md +298 -0
  163. package/skills/rls-design/SKILL.md +535 -0
  164. package/skills/semantic-model/SKILL.md +237 -0
  165. package/skills/testing-validation/SKILL.md +643 -0
  166. package/skills/theme-tweaker/SKILL.md +626 -0
  167. package/src/content/base.md +237 -0
  168. package/src/content/mcp-requirements.json +69 -0
  169. package/src/content/routing.md +203 -0
  170. package/src/content/skills/contributions.md +259 -0
  171. package/src/content/skills/data-model-design.md +462 -0
  172. package/src/content/skills/data-modeling.md +246 -0
  173. package/src/content/skills/data-quality.md +656 -0
  174. package/src/content/skills/dax-doctor.md +242 -0
  175. package/src/content/skills/dax-udf.md +481 -0
  176. package/src/content/skills/dax.md +700 -0
  177. package/src/content/skills/deployment.md +312 -0
  178. package/src/content/skills/excel-formulas.md +455 -0
  179. package/src/content/skills/fabric-scripts.md +446 -0
  180. package/src/content/skills/fast-standard.md +501 -0
  181. package/src/content/skills/governance.md +197 -0
  182. package/src/content/skills/migration-assistant.md +284 -0
  183. package/src/content/skills/model-documenter.md +236 -0
  184. package/src/content/skills/pbi-connect.md +233 -0
  185. package/src/content/skills/power-query.md +398 -0
  186. package/src/content/skills/project-kickoff.md +899 -0
  187. package/src/content/skills/query-performance.md +472 -0
  188. package/src/content/skills/report-design.md +199 -0
  189. package/src/content/skills/report-layout.md +290 -0
  190. package/src/content/skills/rls-design.md +527 -0
  191. package/src/content/skills/semantic-model.md +229 -0
  192. package/src/content/skills/testing-validation.md +635 -0
  193. package/src/content/skills/theme-tweaker.md +618 -0
@@ -0,0 +1,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
+ });