@luquimbo/bi-superpowers 3.1.1 → 4.1.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/marketplace.json +5 -3
- package/.claude-plugin/plugin.json +28 -2
- package/.claude-plugin/skill-manifest.json +22 -6
- package/.plugin/plugin.json +1 -1
- package/AGENTS.md +52 -36
- package/CHANGELOG.md +295 -0
- package/README.md +75 -26
- package/bin/build-plugin.js +17 -10
- package/bin/cli.js +278 -322
- package/bin/commands/build-desktop.js +35 -16
- package/bin/commands/diff.js +31 -13
- package/bin/commands/install.js +93 -72
- package/bin/commands/lint.js +40 -26
- package/bin/commands/mcp-setup.js +3 -10
- package/bin/commands/update-check.js +389 -0
- package/bin/lib/agents.js +19 -0
- package/bin/lib/generators/claude-plugin.js +144 -6
- package/bin/lib/generators/shared.js +29 -33
- package/bin/lib/mcp-config.js +191 -16
- package/bin/lib/skills.js +115 -27
- package/bin/postinstall.js +4 -2
- package/bin/utils/mcp-detect.js +2 -2
- package/commands/bi-start.md +218 -0
- package/commands/pbi-connect.md +43 -65
- package/commands/project-kickoff.md +393 -673
- package/commands/report-design.md +403 -0
- package/desktop-extension/manifest.json +5 -12
- package/desktop-extension/server.js +34 -25
- package/package.json +6 -10
- package/skills/bi-start/SKILL.md +220 -0
- package/skills/bi-start/scripts/update-check.js +389 -0
- package/skills/pbi-connect/SKILL.md +45 -67
- package/skills/pbi-connect/scripts/update-check.js +389 -0
- package/skills/project-kickoff/SKILL.md +395 -675
- package/skills/project-kickoff/scripts/update-check.js +389 -0
- package/skills/report-design/SKILL.md +405 -0
- package/skills/report-design/references/cli-commands.md +184 -0
- package/skills/report-design/references/cli-setup.md +101 -0
- package/skills/report-design/references/close-write-open-pattern.md +80 -0
- package/skills/report-design/references/layouts/finance.md +65 -0
- package/skills/report-design/references/layouts/generic.md +46 -0
- package/skills/report-design/references/layouts/hr.md +48 -0
- package/skills/report-design/references/layouts/marketing.md +45 -0
- package/skills/report-design/references/layouts/operations.md +44 -0
- package/skills/report-design/references/layouts/sales.md +50 -0
- package/skills/report-design/references/native-visuals.md +341 -0
- package/skills/report-design/references/pbi-desktop-installation.md +87 -0
- package/skills/report-design/references/pbir-preview-activation.md +40 -0
- package/skills/report-design/references/slicer.md +89 -0
- package/skills/report-design/references/textbox.md +101 -0
- package/skills/report-design/references/themes/BISuperpowers.json +915 -0
- package/skills/report-design/references/troubleshooting.md +135 -0
- package/skills/report-design/references/visual-types.md +78 -0
- package/skills/report-design/scripts/apply-theme.js +243 -0
- package/skills/report-design/scripts/create-visual.js +878 -0
- package/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
- package/skills/report-design/scripts/update-check.js +389 -0
- package/skills/report-design/scripts/validate-pbir.js +322 -0
- package/src/content/base.md +12 -68
- package/src/content/mcp-requirements.json +0 -25
- package/src/content/routing.md +19 -74
- package/src/content/skills/bi-start.md +191 -0
- package/src/content/skills/pbi-connect.md +22 -65
- package/src/content/skills/project-kickoff.md +372 -673
- package/src/content/skills/report-design/SKILL.md +376 -0
- package/src/content/skills/report-design/references/cli-commands.md +184 -0
- package/src/content/skills/report-design/references/cli-setup.md +101 -0
- package/src/content/skills/report-design/references/close-write-open-pattern.md +80 -0
- package/src/content/skills/report-design/references/layouts/finance.md +65 -0
- package/src/content/skills/report-design/references/layouts/generic.md +46 -0
- package/src/content/skills/report-design/references/layouts/hr.md +48 -0
- package/src/content/skills/report-design/references/layouts/marketing.md +45 -0
- package/src/content/skills/report-design/references/layouts/operations.md +44 -0
- package/src/content/skills/report-design/references/layouts/sales.md +50 -0
- package/src/content/skills/report-design/references/native-visuals.md +341 -0
- package/src/content/skills/report-design/references/pbi-desktop-installation.md +87 -0
- package/src/content/skills/report-design/references/pbir-preview-activation.md +40 -0
- package/src/content/skills/report-design/references/slicer.md +89 -0
- package/src/content/skills/report-design/references/textbox.md +101 -0
- package/src/content/skills/report-design/references/themes/BISuperpowers.json +915 -0
- package/src/content/skills/report-design/references/troubleshooting.md +135 -0
- package/src/content/skills/report-design/references/visual-types.md +78 -0
- package/src/content/skills/report-design/scripts/apply-theme.js +243 -0
- package/src/content/skills/report-design/scripts/create-visual.js +878 -0
- package/src/content/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
- package/src/content/skills/report-design/scripts/validate-pbir.js +322 -0
- package/bin/commands/add.js +0 -533
- package/bin/commands/add.test.js +0 -77
- package/bin/commands/changelog.js +0 -443
- package/bin/commands/install.test.js +0 -289
- package/bin/commands/lint.test.js +0 -103
- package/bin/commands/pull.js +0 -287
- package/bin/commands/pull.test.js +0 -36
- package/bin/commands/push.js +0 -231
- package/bin/commands/push.test.js +0 -14
- package/bin/commands/search.js +0 -344
- package/bin/commands/search.test.js +0 -115
- package/bin/commands/setup.js +0 -545
- package/bin/commands/setup.test.js +0 -46
- package/bin/commands/sync-profile.js +0 -405
- package/bin/commands/sync-profile.test.js +0 -14
- package/bin/commands/sync-source.js +0 -418
- package/bin/commands/sync-source.test.js +0 -14
- package/bin/lib/generators/claude-plugin.test.js +0 -111
- package/bin/lib/mcp-config.test.js +0 -310
- package/bin/lib/microsoft-mcp.test.js +0 -115
- package/bin/utils/errors.js +0 -159
- package/bin/utils/git.js +0 -298
- package/bin/utils/logger.js +0 -142
- package/bin/utils/mcp-detect.test.js +0 -81
- package/bin/utils/pbix.js +0 -305
- package/bin/utils/pbix.test.js +0 -37
- package/bin/utils/profiles.js +0 -312
- package/bin/utils/projects.js +0 -169
- package/bin/utils/readline.js +0 -206
- package/bin/utils/readline.test.js +0 -47
- package/bin/utils/tui.test.js +0 -127
- package/docs/openrouter-free-models.md +0 -92
- package/library/examples/README.md +0 -151
- package/library/examples/finance-reporting/README.md +0 -351
- package/library/examples/finance-reporting/data-model.md +0 -267
- package/library/examples/finance-reporting/measures.dax +0 -557
- package/library/examples/hr-analytics/README.md +0 -371
- package/library/examples/hr-analytics/data-model.md +0 -315
- package/library/examples/hr-analytics/measures.dax +0 -460
- package/library/examples/marketing-analytics/README.md +0 -37
- package/library/examples/marketing-analytics/data-model.md +0 -62
- package/library/examples/marketing-analytics/measures.dax +0 -110
- package/library/examples/retail-analytics/README.md +0 -439
- package/library/examples/retail-analytics/data-model.md +0 -288
- package/library/examples/retail-analytics/measures.dax +0 -481
- package/library/examples/supply-chain/README.md +0 -37
- package/library/examples/supply-chain/data-model.md +0 -69
- package/library/examples/supply-chain/measures.dax +0 -77
- package/library/examples/udf-library/README.md +0 -228
- package/library/examples/udf-library/functions.dax +0 -571
- package/library/snippets/dax/README.md +0 -292
- package/library/snippets/dax/business-domains.md +0 -576
- package/library/snippets/dax/calculate-patterns.md +0 -276
- package/library/snippets/dax/calculation-groups.md +0 -489
- package/library/snippets/dax/error-handling.md +0 -495
- package/library/snippets/dax/iterators-and-aggregations.md +0 -474
- package/library/snippets/dax/kpis-and-metrics.md +0 -293
- package/library/snippets/dax/rankings-and-topn.md +0 -235
- package/library/snippets/dax/security-patterns.md +0 -413
- package/library/snippets/dax/text-and-formatting.md +0 -316
- package/library/snippets/dax/time-intelligence.md +0 -196
- package/library/snippets/dax/user-defined-functions.md +0 -477
- package/library/snippets/dax/virtual-tables.md +0 -546
- package/library/snippets/excel-formulas/README.md +0 -84
- package/library/snippets/excel-formulas/aggregations.md +0 -330
- package/library/snippets/excel-formulas/dates-and-times.md +0 -361
- package/library/snippets/excel-formulas/dynamic-arrays.md +0 -314
- package/library/snippets/excel-formulas/lookups.md +0 -169
- package/library/snippets/excel-formulas/text-functions.md +0 -363
- package/library/snippets/governance/naming-conventions.md +0 -97
- package/library/snippets/governance/review-checklists.md +0 -107
- package/library/snippets/power-query/README.md +0 -389
- package/library/snippets/power-query/api-integration.md +0 -707
- package/library/snippets/power-query/connections.md +0 -434
- package/library/snippets/power-query/data-cleaning.md +0 -298
- package/library/snippets/power-query/error-handling.md +0 -526
- package/library/snippets/power-query/parameters.md +0 -350
- package/library/snippets/power-query/performance.md +0 -506
- package/library/snippets/power-query/transformations.md +0 -330
- package/library/snippets/report-design/accessibility.md +0 -78
- package/library/snippets/report-design/chart-selection.md +0 -54
- package/library/snippets/report-design/layout-patterns.md +0 -87
- package/library/templates/data-models/README.md +0 -93
- package/library/templates/data-models/finance-model.md +0 -627
- package/library/templates/data-models/retail-star-schema.md +0 -473
- package/library/templates/excel/README.md +0 -83
- package/library/templates/excel/budget-tracker.md +0 -432
- package/library/templates/excel/data-entry-form.md +0 -533
- package/library/templates/power-bi/README.md +0 -72
- package/library/templates/power-bi/finance-report.md +0 -449
- package/library/templates/power-bi/kpi-scorecard.md +0 -461
- package/library/templates/power-bi/sales-dashboard.md +0 -281
- package/library/themes/excel/README.md +0 -436
- package/library/themes/power-bi/README.md +0 -271
- package/library/themes/power-bi/accessible.json +0 -307
- package/library/themes/power-bi/bi-superpowers-default.json +0 -858
- package/library/themes/power-bi/corporate-blue.json +0 -291
- package/library/themes/power-bi/dark-mode.json +0 -291
- package/library/themes/power-bi/minimal.json +0 -292
- package/library/themes/power-bi/print-friendly.json +0 -309
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the multi-agent MCP config writer.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
6
|
-
const assert = require('node:assert');
|
|
7
|
-
const fs = require('node:fs');
|
|
8
|
-
const os = require('node:os');
|
|
9
|
-
const path = require('node:path');
|
|
10
|
-
|
|
11
|
-
const {
|
|
12
|
-
MCP_WRITERS,
|
|
13
|
-
MODELING_SERVER_NAME,
|
|
14
|
-
LEARN_SERVER_NAME,
|
|
15
|
-
MICROSOFT_LEARN_URL,
|
|
16
|
-
tomlEscape,
|
|
17
|
-
escapeRegex,
|
|
18
|
-
assertNotSymlink,
|
|
19
|
-
getLauncherAbsolutePath,
|
|
20
|
-
} = require('./mcp-config');
|
|
21
|
-
|
|
22
|
-
describe('mcp-config helpers', () => {
|
|
23
|
-
test('getLauncherAbsolutePath returns absolute launcher path', () => {
|
|
24
|
-
const pkgDir = '/tmp/fake-pkg';
|
|
25
|
-
const launcher = getLauncherAbsolutePath(pkgDir);
|
|
26
|
-
assert.strictEqual(launcher, path.join(pkgDir, 'bin', 'mcp', 'powerbi-modeling-launcher.js'));
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('tomlEscape escapes backslashes and quotes', () => {
|
|
30
|
-
assert.strictEqual(tomlEscape('plain'), 'plain');
|
|
31
|
-
assert.strictEqual(tomlEscape('C:\\path\\to\\file'), 'C:\\\\path\\\\to\\\\file');
|
|
32
|
-
assert.strictEqual(tomlEscape('say "hello"'), 'say \\"hello\\"');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('tomlEscape escapes TOML control characters', () => {
|
|
36
|
-
assert.strictEqual(tomlEscape('line1\nline2'), 'line1\\nline2');
|
|
37
|
-
assert.strictEqual(tomlEscape('tab\there'), 'tab\\there');
|
|
38
|
-
assert.strictEqual(tomlEscape('cr\rhere'), 'cr\\rhere');
|
|
39
|
-
// \x08 = backspace, \f = form feed
|
|
40
|
-
assert.strictEqual(tomlEscape('\x08\f'), '\\b\\f');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('escapeRegex escapes all regex metacharacters', () => {
|
|
44
|
-
assert.strictEqual(escapeRegex('plain'), 'plain');
|
|
45
|
-
assert.strictEqual(escapeRegex('a.b'), 'a\\.b');
|
|
46
|
-
assert.strictEqual(escapeRegex('a[b]c'), 'a\\[b\\]c');
|
|
47
|
-
assert.strictEqual(escapeRegex('a+b*c?'), 'a\\+b\\*c\\?');
|
|
48
|
-
assert.strictEqual(escapeRegex('(hello)'), '\\(hello\\)');
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe('assertNotSymlink', () => {
|
|
53
|
-
let tempDir;
|
|
54
|
-
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bi-mcp-symlink-'));
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
afterEach(() => {
|
|
60
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('allows non-existent paths', () => {
|
|
64
|
-
assert.doesNotThrow(() => {
|
|
65
|
-
assertNotSymlink(path.join(tempDir, 'nope.json'));
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('allows regular files', () => {
|
|
70
|
-
const file = path.join(tempDir, 'regular.json');
|
|
71
|
-
fs.writeFileSync(file, '{}');
|
|
72
|
-
assert.doesNotThrow(() => assertNotSymlink(file));
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('throws on symbolic links', () => {
|
|
76
|
-
const target = path.join(tempDir, 'target.json');
|
|
77
|
-
const link = path.join(tempDir, 'link.json');
|
|
78
|
-
fs.writeFileSync(target, '{}');
|
|
79
|
-
fs.symlinkSync(target, link);
|
|
80
|
-
|
|
81
|
-
assert.throws(() => assertNotSymlink(link), /is a symbolic link/);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('MCP_WRITERS registry', () => {
|
|
86
|
-
test('covers all 5 supported agents', () => {
|
|
87
|
-
const ids = Object.keys(MCP_WRITERS).sort();
|
|
88
|
-
assert.deepStrictEqual(ids, ['claude-code', 'codex', 'gemini-cli', 'github-copilot', 'kilo']);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('each writer is a function', () => {
|
|
92
|
-
for (const [id, writer] of Object.entries(MCP_WRITERS)) {
|
|
93
|
-
assert.strictEqual(typeof writer, 'function', `${id} writer should be a function`);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe('MCP writers — JSON agents', () => {
|
|
99
|
-
let tempHome;
|
|
100
|
-
let origHome;
|
|
101
|
-
const fakePkgDir = '/tmp/fake-bi-superpowers';
|
|
102
|
-
|
|
103
|
-
beforeEach(() => {
|
|
104
|
-
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'bi-mcp-home-'));
|
|
105
|
-
origHome = os.homedir;
|
|
106
|
-
os.homedir = () => tempHome;
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
afterEach(() => {
|
|
110
|
-
os.homedir = origHome;
|
|
111
|
-
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('claude-code writer produces mcpServers JSON at ~/.claude.json', () => {
|
|
115
|
-
const configPath = MCP_WRITERS['claude-code'](fakePkgDir);
|
|
116
|
-
|
|
117
|
-
assert.strictEqual(configPath, path.join(tempHome, '.claude.json'));
|
|
118
|
-
assert.ok(fs.existsSync(configPath));
|
|
119
|
-
|
|
120
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
121
|
-
assert.ok(config.mcpServers);
|
|
122
|
-
assert.ok(config.mcpServers[MODELING_SERVER_NAME]);
|
|
123
|
-
assert.strictEqual(config.mcpServers[MODELING_SERVER_NAME].command, 'node');
|
|
124
|
-
assert.ok(
|
|
125
|
-
config.mcpServers[MODELING_SERVER_NAME].args[0].includes('powerbi-modeling-launcher.js')
|
|
126
|
-
);
|
|
127
|
-
assert.deepStrictEqual(config.mcpServers[LEARN_SERVER_NAME], {
|
|
128
|
-
type: 'http',
|
|
129
|
-
url: MICROSOFT_LEARN_URL,
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('github-copilot writer uses "servers" key (not mcpServers)', () => {
|
|
134
|
-
const configPath = MCP_WRITERS['github-copilot'](fakePkgDir);
|
|
135
|
-
|
|
136
|
-
assert.strictEqual(configPath, path.join(tempHome, '.copilot', 'mcp-config.json'));
|
|
137
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
138
|
-
assert.ok(config.servers);
|
|
139
|
-
assert.strictEqual(config.mcpServers, undefined);
|
|
140
|
-
assert.strictEqual(config.servers[MODELING_SERVER_NAME].type, 'stdio');
|
|
141
|
-
assert.strictEqual(config.servers[LEARN_SERVER_NAME].type, 'http');
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test('gemini-cli writer uses httpUrl (not url)', () => {
|
|
145
|
-
const configPath = MCP_WRITERS['gemini-cli'](fakePkgDir);
|
|
146
|
-
|
|
147
|
-
assert.strictEqual(configPath, path.join(tempHome, '.gemini', 'settings.json'));
|
|
148
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
149
|
-
assert.ok(config.mcpServers);
|
|
150
|
-
assert.strictEqual(config.mcpServers[LEARN_SERVER_NAME].httpUrl, MICROSOFT_LEARN_URL);
|
|
151
|
-
assert.strictEqual(config.mcpServers[LEARN_SERVER_NAME].url, undefined);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test('kilo writer uses url (standard)', () => {
|
|
155
|
-
const configPath = MCP_WRITERS.kilo(fakePkgDir);
|
|
156
|
-
|
|
157
|
-
assert.strictEqual(configPath, path.join(tempHome, '.kilo', 'mcp_settings.json'));
|
|
158
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
159
|
-
assert.ok(config.mcpServers);
|
|
160
|
-
assert.strictEqual(config.mcpServers[LEARN_SERVER_NAME].url, MICROSOFT_LEARN_URL);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('claude-code writer preserves existing config', () => {
|
|
164
|
-
const configPath = path.join(tempHome, '.claude.json');
|
|
165
|
-
fs.writeFileSync(
|
|
166
|
-
configPath,
|
|
167
|
-
JSON.stringify({
|
|
168
|
-
otherSetting: 'value',
|
|
169
|
-
mcpServers: { 'existing-mcp': { command: 'echo' } },
|
|
170
|
-
})
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
MCP_WRITERS['claude-code'](fakePkgDir);
|
|
174
|
-
|
|
175
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
176
|
-
assert.strictEqual(config.otherSetting, 'value');
|
|
177
|
-
assert.ok(config.mcpServers['existing-mcp']);
|
|
178
|
-
assert.ok(config.mcpServers[MODELING_SERVER_NAME]);
|
|
179
|
-
assert.ok(config.mcpServers[LEARN_SERVER_NAME]);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Re-install de-duplication for JSON writers — each should replace its
|
|
183
|
-
// own entries cleanly without duplicating them on repeated runs.
|
|
184
|
-
for (const agentId of ['claude-code', 'github-copilot', 'gemini-cli', 'kilo']) {
|
|
185
|
-
test(`${agentId} re-install replaces entries without duplicating`, () => {
|
|
186
|
-
// First install
|
|
187
|
-
MCP_WRITERS[agentId](fakePkgDir);
|
|
188
|
-
// Second install (same args)
|
|
189
|
-
const configPath = MCP_WRITERS[agentId](fakePkgDir);
|
|
190
|
-
|
|
191
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
192
|
-
const serversKey = agentId === 'github-copilot' ? 'servers' : 'mcpServers';
|
|
193
|
-
const servers = config[serversKey];
|
|
194
|
-
|
|
195
|
-
// Our 2 servers should each appear exactly once (via object keys)
|
|
196
|
-
assert.ok(servers[MODELING_SERVER_NAME], 'modeling server missing after re-install');
|
|
197
|
-
assert.ok(servers[LEARN_SERVER_NAME], 'learn server missing after re-install');
|
|
198
|
-
|
|
199
|
-
// And no stray duplicates of our keys
|
|
200
|
-
const ourKeys = Object.keys(servers).filter(
|
|
201
|
-
(k) => k === MODELING_SERVER_NAME || k === LEARN_SERVER_NAME
|
|
202
|
-
);
|
|
203
|
-
assert.strictEqual(ourKeys.length, 2, `expected 2 of our keys, found ${ourKeys.length}`);
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Cross-agent sanity check: verifies each JSON agent writes the HTTP
|
|
208
|
-
// URL under the correct key per its spec. Catches copy-paste errors
|
|
209
|
-
// where someone might accidentally use `url` for Gemini (which expects
|
|
210
|
-
// `httpUrl`) or vice versa.
|
|
211
|
-
test('each JSON agent uses the correct HTTP field name', () => {
|
|
212
|
-
const expectations = {
|
|
213
|
-
'claude-code': { key: 'url', wrapperKey: 'mcpServers' },
|
|
214
|
-
'github-copilot': { key: 'url', wrapperKey: 'servers' },
|
|
215
|
-
'gemini-cli': { key: 'httpUrl', wrapperKey: 'mcpServers' },
|
|
216
|
-
kilo: { key: 'url', wrapperKey: 'mcpServers' },
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
for (const [agentId, expected] of Object.entries(expectations)) {
|
|
220
|
-
// Reset home for each agent so configs don't leak between writes.
|
|
221
|
-
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
222
|
-
fs.mkdirSync(tempHome, { recursive: true });
|
|
223
|
-
|
|
224
|
-
const configPath = MCP_WRITERS[agentId](fakePkgDir);
|
|
225
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
226
|
-
const learnEntry = config[expected.wrapperKey][LEARN_SERVER_NAME];
|
|
227
|
-
|
|
228
|
-
assert.strictEqual(
|
|
229
|
-
learnEntry[expected.key],
|
|
230
|
-
MICROSOFT_LEARN_URL,
|
|
231
|
-
`${agentId} should use "${expected.key}" for HTTP URL`
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
// And it should NOT use the wrong key (defensive check)
|
|
235
|
-
const wrongKey = expected.key === 'url' ? 'httpUrl' : 'url';
|
|
236
|
-
assert.strictEqual(
|
|
237
|
-
learnEntry[wrongKey],
|
|
238
|
-
undefined,
|
|
239
|
-
`${agentId} should NOT have "${wrongKey}"`
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
test('writeJson refuses to overwrite symlinked target', () => {
|
|
245
|
-
const realFile = path.join(tempHome, 'real-claude.json');
|
|
246
|
-
const linkFile = path.join(tempHome, '.claude.json');
|
|
247
|
-
fs.writeFileSync(realFile, '{"mcpServers":{}}');
|
|
248
|
-
fs.symlinkSync(realFile, linkFile);
|
|
249
|
-
|
|
250
|
-
assert.throws(() => MCP_WRITERS['claude-code'](fakePkgDir), /symbolic link/);
|
|
251
|
-
|
|
252
|
-
// The real file content must not have been touched.
|
|
253
|
-
const content = JSON.parse(fs.readFileSync(realFile, 'utf8'));
|
|
254
|
-
assert.deepStrictEqual(content, { mcpServers: {} });
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
describe('codex writer — TOML', () => {
|
|
259
|
-
let tempHome;
|
|
260
|
-
let origHome;
|
|
261
|
-
|
|
262
|
-
beforeEach(() => {
|
|
263
|
-
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'bi-mcp-codex-'));
|
|
264
|
-
origHome = os.homedir;
|
|
265
|
-
os.homedir = () => tempHome;
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
afterEach(() => {
|
|
269
|
-
os.homedir = origHome;
|
|
270
|
-
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test('writes TOML with [mcp_servers.*] sections', () => {
|
|
274
|
-
const configPath = MCP_WRITERS.codex('/tmp/fake-pkg');
|
|
275
|
-
|
|
276
|
-
assert.strictEqual(configPath, path.join(tempHome, '.codex', 'config.toml'));
|
|
277
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
278
|
-
|
|
279
|
-
assert.ok(content.includes('[mcp_servers.powerbi-modeling]'));
|
|
280
|
-
assert.ok(content.includes('command = "node"'));
|
|
281
|
-
assert.ok(content.includes('[mcp_servers.microsoft-learn]'));
|
|
282
|
-
assert.ok(content.includes(`url = "${MICROSOFT_LEARN_URL}"`));
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test('preserves existing non-MCP TOML content', () => {
|
|
286
|
-
const configPath = path.join(tempHome, '.codex', 'config.toml');
|
|
287
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
288
|
-
fs.writeFileSync(configPath, '[profile]\nname = "default"\n');
|
|
289
|
-
|
|
290
|
-
MCP_WRITERS.codex('/tmp/fake-pkg');
|
|
291
|
-
|
|
292
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
293
|
-
assert.ok(content.includes('[profile]'));
|
|
294
|
-
assert.ok(content.includes('name = "default"'));
|
|
295
|
-
assert.ok(content.includes('[mcp_servers.powerbi-modeling]'));
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
test('re-running install replaces MCP sections, does not duplicate', () => {
|
|
299
|
-
MCP_WRITERS.codex('/tmp/fake-pkg');
|
|
300
|
-
MCP_WRITERS.codex('/tmp/fake-pkg');
|
|
301
|
-
|
|
302
|
-
const content = fs.readFileSync(path.join(tempHome, '.codex', 'config.toml'), 'utf8');
|
|
303
|
-
|
|
304
|
-
// Each section name should appear exactly once
|
|
305
|
-
const modelingCount = (content.match(/\[mcp_servers\.powerbi-modeling\]/g) || []).length;
|
|
306
|
-
const learnCount = (content.match(/\[mcp_servers\.microsoft-learn\]/g) || []).length;
|
|
307
|
-
assert.strictEqual(modelingCount, 1);
|
|
308
|
-
assert.strictEqual(learnCount, 1);
|
|
309
|
-
});
|
|
310
|
-
});
|
|
@@ -1,115 +0,0 @@
|
|
|
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
|
-
MICROSOFT_LEARN_URL,
|
|
13
|
-
createPluginMcpConfig,
|
|
14
|
-
createMcpConfigForFormat,
|
|
15
|
-
mergeMcpConfig,
|
|
16
|
-
resolveModelingLauncherPath,
|
|
17
|
-
} = require('./microsoft-mcp');
|
|
18
|
-
|
|
19
|
-
describe('resolveModelingLauncherPath', () => {
|
|
20
|
-
test('uses plugin root placeholder for plugin builds', () => {
|
|
21
|
-
const launcherPath = resolveModelingLauncherPath({
|
|
22
|
-
launcherMode: PLUGIN_ROOT_LAUNCHER_MODE,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
assert.strictEqual(launcherPath, '${CLAUDE_PLUGIN_ROOT}/bin/mcp/powerbi-modeling-launcher.js');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('uses absolute package path for generated project configs', () => {
|
|
29
|
-
const launcherPath = resolveModelingLauncherPath({
|
|
30
|
-
packageDir: '/tmp/bi-superpowers',
|
|
31
|
-
launcherMode: ABSOLUTE_LAUNCHER_MODE,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
assert.strictEqual(
|
|
35
|
-
launcherPath,
|
|
36
|
-
path.join('/tmp/bi-superpowers', 'bin', 'mcp', 'powerbi-modeling-launcher.js')
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe('createPluginMcpConfig', () => {
|
|
42
|
-
test('returns the 2 official Microsoft MCP defaults', () => {
|
|
43
|
-
const config = createPluginMcpConfig({
|
|
44
|
-
packageDir: '/tmp/bi-superpowers',
|
|
45
|
-
launcherMode: ABSOLUTE_LAUNCHER_MODE,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// Should have exactly 2 servers
|
|
49
|
-
assert.strictEqual(Object.keys(config).length, 2);
|
|
50
|
-
|
|
51
|
-
// powerbi-modeling-mcp: local stdio launcher
|
|
52
|
-
assert.strictEqual(config['powerbi-modeling-mcp'].type, 'stdio');
|
|
53
|
-
assert.strictEqual(config['powerbi-modeling-mcp'].command, 'node');
|
|
54
|
-
assert.ok(config['powerbi-modeling-mcp'].args[0].endsWith('powerbi-modeling-launcher.js'));
|
|
55
|
-
|
|
56
|
-
// microsoft-learn: HTTP MCP
|
|
57
|
-
assert.deepStrictEqual(config['microsoft-learn'], {
|
|
58
|
-
type: 'http',
|
|
59
|
-
url: MICROSOFT_LEARN_URL,
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('does not include the old removed servers', () => {
|
|
64
|
-
const config = createPluginMcpConfig({
|
|
65
|
-
packageDir: '/tmp/bi-superpowers',
|
|
66
|
-
launcherMode: ABSOLUTE_LAUNCHER_MODE,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
assert.strictEqual(config['powerbi-remote'], undefined);
|
|
70
|
-
assert.strictEqual(config['fabric-mcp-server'], undefined);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe('createMcpConfigForFormat', () => {
|
|
75
|
-
test('returns flat plugin config for plugin/cursor outputs', () => {
|
|
76
|
-
const pluginConfig = createMcpConfigForFormat('plugin');
|
|
77
|
-
const cursorConfig = createMcpConfigForFormat('cursor');
|
|
78
|
-
|
|
79
|
-
assert.ok(pluginConfig['powerbi-modeling-mcp']);
|
|
80
|
-
assert.ok(cursorConfig['microsoft-learn']);
|
|
81
|
-
assert.strictEqual(pluginConfig['microsoft-learn'].type, 'http');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('returns nested mcpServers config for claude-like outputs', () => {
|
|
85
|
-
const config = createMcpConfigForFormat('claude');
|
|
86
|
-
|
|
87
|
-
assert.ok(config.mcpServers);
|
|
88
|
-
assert.ok(config.mcpServers['powerbi-modeling-mcp']);
|
|
89
|
-
assert.ok(config.mcpServers['microsoft-learn']);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe('mergeMcpConfig', () => {
|
|
94
|
-
test('merges plugin configs without dropping existing entries', () => {
|
|
95
|
-
const merged = mergeMcpConfig(
|
|
96
|
-
{ existing: { type: 'http', url: 'https://example.com' } },
|
|
97
|
-
{ 'microsoft-learn': { type: 'http', url: MICROSOFT_LEARN_URL } },
|
|
98
|
-
'plugin'
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
assert.ok(merged.existing);
|
|
102
|
-
assert.ok(merged['microsoft-learn']);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test('merges claude configs inside mcpServers', () => {
|
|
106
|
-
const merged = mergeMcpConfig(
|
|
107
|
-
{ mcpServers: { existing: { type: 'http', url: 'https://example.com' } } },
|
|
108
|
-
{ mcpServers: { 'microsoft-learn': { type: 'http', url: MICROSOFT_LEARN_URL } } },
|
|
109
|
-
'claude'
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
assert.ok(merged.mcpServers.existing);
|
|
113
|
-
assert.ok(merged.mcpServers['microsoft-learn']);
|
|
114
|
-
});
|
|
115
|
-
});
|
package/bin/utils/errors.js
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Error Handling Utilities
|
|
3
|
-
* ========================
|
|
4
|
-
*
|
|
5
|
-
* Standardized error handling for the CLI.
|
|
6
|
-
* Provides consistent error messages and exit codes.
|
|
7
|
-
*
|
|
8
|
-
* @module utils/errors
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Custom CLI Error class
|
|
13
|
-
* Extends Error with exit code and optional suggestion
|
|
14
|
-
*/
|
|
15
|
-
class CLIError extends Error {
|
|
16
|
-
/**
|
|
17
|
-
* Create a CLI error
|
|
18
|
-
* @param {string} message - Error message
|
|
19
|
-
* @param {number} code - Exit code (default: 1)
|
|
20
|
-
* @param {string|null} suggestion - Optional suggestion for fixing the error
|
|
21
|
-
*/
|
|
22
|
-
constructor(message, code = 1, suggestion = null) {
|
|
23
|
-
super(message);
|
|
24
|
-
this.name = 'CLIError';
|
|
25
|
-
this.code = code;
|
|
26
|
-
this.suggestion = suggestion;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Error codes for common scenarios
|
|
32
|
-
*/
|
|
33
|
-
const ErrorCodes = {
|
|
34
|
-
GENERAL: 1,
|
|
35
|
-
INVALID_INPUT: 2,
|
|
36
|
-
FILE_NOT_FOUND: 3,
|
|
37
|
-
PERMISSION_DENIED: 4,
|
|
38
|
-
NETWORK_ERROR: 5,
|
|
39
|
-
CONFIG_ERROR: 6,
|
|
40
|
-
GIT_ERROR: 7,
|
|
41
|
-
VALIDATION_ERROR: 8,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Handle an error with consistent formatting
|
|
46
|
-
* @param {Error} error - The error to handle
|
|
47
|
-
* @param {Object} options - Options for handling
|
|
48
|
-
* @param {boolean} options.exit - Whether to exit the process (default: true)
|
|
49
|
-
* @param {boolean} options.verbose - Show stack trace (default: false)
|
|
50
|
-
*/
|
|
51
|
-
function handleError(error, options = {}) {
|
|
52
|
-
const { exit = true, verbose = process.env.DEBUG === 'true' } = options;
|
|
53
|
-
|
|
54
|
-
if (error instanceof CLIError) {
|
|
55
|
-
console.error(`Error: ${error.message}`);
|
|
56
|
-
if (error.suggestion) {
|
|
57
|
-
console.error(`\nSuggestion: ${error.suggestion}`);
|
|
58
|
-
}
|
|
59
|
-
if (exit) {
|
|
60
|
-
process.exit(error.code);
|
|
61
|
-
}
|
|
62
|
-
} else if (error instanceof Error) {
|
|
63
|
-
console.error(`Error: ${error.message}`);
|
|
64
|
-
if (verbose && error.stack) {
|
|
65
|
-
console.error(`\nStack trace:\n${error.stack}`);
|
|
66
|
-
}
|
|
67
|
-
if (exit) {
|
|
68
|
-
process.exit(ErrorCodes.GENERAL);
|
|
69
|
-
}
|
|
70
|
-
} else {
|
|
71
|
-
console.error(`Error: ${String(error)}`);
|
|
72
|
-
if (exit) {
|
|
73
|
-
process.exit(ErrorCodes.GENERAL);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Wrap an async function with error handling
|
|
80
|
-
* @param {Function} fn - Async function to wrap
|
|
81
|
-
* @param {Object} options - Error handling options
|
|
82
|
-
* @returns {Function} Wrapped function
|
|
83
|
-
*/
|
|
84
|
-
function withErrorHandling(fn, options = {}) {
|
|
85
|
-
return async (...args) => {
|
|
86
|
-
try {
|
|
87
|
-
return await fn(...args);
|
|
88
|
-
} catch (error) {
|
|
89
|
-
handleError(error, options);
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Log a warning without exiting
|
|
96
|
-
* @param {string} message - Warning message
|
|
97
|
-
* @param {Object} context - Additional context (optional)
|
|
98
|
-
*/
|
|
99
|
-
function logWarning(message, context = null) {
|
|
100
|
-
console.warn(`Warning: ${message}`);
|
|
101
|
-
if (context && process.env.DEBUG === 'true') {
|
|
102
|
-
console.warn('Context:', context);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Create a CLIError for common scenarios
|
|
108
|
-
*/
|
|
109
|
-
const createError = {
|
|
110
|
-
fileNotFound: (path) =>
|
|
111
|
-
new CLIError(
|
|
112
|
-
`File not found: ${path}`,
|
|
113
|
-
ErrorCodes.FILE_NOT_FOUND,
|
|
114
|
-
'Check that the file path is correct.'
|
|
115
|
-
),
|
|
116
|
-
|
|
117
|
-
invalidInput: (message, suggestion = null) =>
|
|
118
|
-
new CLIError(message, ErrorCodes.INVALID_INPUT, suggestion),
|
|
119
|
-
|
|
120
|
-
configError: (message) =>
|
|
121
|
-
new CLIError(
|
|
122
|
-
message,
|
|
123
|
-
ErrorCodes.CONFIG_ERROR,
|
|
124
|
-
'Run "bi-superpowers setup" to configure the repository.'
|
|
125
|
-
),
|
|
126
|
-
|
|
127
|
-
gitError: (message) =>
|
|
128
|
-
new CLIError(
|
|
129
|
-
message,
|
|
130
|
-
ErrorCodes.GIT_ERROR,
|
|
131
|
-
'Make sure Git is installed and you are in a Git repository.'
|
|
132
|
-
),
|
|
133
|
-
|
|
134
|
-
networkError: (message) =>
|
|
135
|
-
new CLIError(
|
|
136
|
-
message,
|
|
137
|
-
ErrorCodes.NETWORK_ERROR,
|
|
138
|
-
'Check your internet connection and try again.'
|
|
139
|
-
),
|
|
140
|
-
|
|
141
|
-
permissionDenied: (path) =>
|
|
142
|
-
new CLIError(
|
|
143
|
-
`Permission denied: ${path}`,
|
|
144
|
-
ErrorCodes.PERMISSION_DENIED,
|
|
145
|
-
'Check file permissions.'
|
|
146
|
-
),
|
|
147
|
-
|
|
148
|
-
validationError: (message, suggestion = null) =>
|
|
149
|
-
new CLIError(message, ErrorCodes.VALIDATION_ERROR, suggestion),
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
module.exports = {
|
|
153
|
-
CLIError,
|
|
154
|
-
ErrorCodes,
|
|
155
|
-
handleError,
|
|
156
|
-
withErrorHandling,
|
|
157
|
-
logWarning,
|
|
158
|
-
createError,
|
|
159
|
-
};
|