@luquimbo/bi-superpowers 3.2.0 → 4.1.1

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 (91) hide show
  1. package/.claude-plugin/marketplace.json +5 -3
  2. package/.claude-plugin/plugin.json +28 -2
  3. package/.claude-plugin/skill-manifest.json +22 -6
  4. package/.plugin/plugin.json +1 -1
  5. package/AGENTS.md +53 -36
  6. package/CHANGELOG.md +310 -0
  7. package/README.md +77 -26
  8. package/bin/build-plugin.js +11 -4
  9. package/bin/cli.js +113 -16
  10. package/bin/commands/build-desktop.js +35 -16
  11. package/bin/commands/diff.js +31 -13
  12. package/bin/commands/install.js +7 -3
  13. package/bin/commands/lint.js +40 -26
  14. package/bin/commands/mcp-setup.js +3 -10
  15. package/bin/commands/update-check.js +403 -0
  16. package/bin/lib/generators/claude-plugin.js +162 -6
  17. package/bin/lib/generators/shared.js +29 -33
  18. package/bin/lib/mcp-config.js +168 -12
  19. package/bin/lib/skills.js +115 -27
  20. package/bin/postinstall.js +4 -2
  21. package/bin/utils/mcp-detect.js +2 -2
  22. package/commands/bi-start.md +197 -0
  23. package/commands/pbi-connect.md +43 -65
  24. package/commands/project-kickoff.md +393 -673
  25. package/commands/report-design.md +403 -0
  26. package/desktop-extension/manifest.json +3 -3
  27. package/package.json +7 -5
  28. package/skills/bi-start/SKILL.md +199 -0
  29. package/skills/bi-start/scripts/update-check.js +403 -0
  30. package/skills/pbi-connect/SKILL.md +45 -67
  31. package/skills/pbi-connect/scripts/update-check.js +403 -0
  32. package/skills/project-kickoff/SKILL.md +395 -675
  33. package/skills/project-kickoff/scripts/update-check.js +403 -0
  34. package/skills/report-design/SKILL.md +405 -0
  35. package/skills/report-design/references/cli-commands.md +184 -0
  36. package/skills/report-design/references/cli-setup.md +101 -0
  37. package/skills/report-design/references/close-write-open-pattern.md +80 -0
  38. package/skills/report-design/references/layouts/finance.md +65 -0
  39. package/skills/report-design/references/layouts/generic.md +46 -0
  40. package/skills/report-design/references/layouts/hr.md +48 -0
  41. package/skills/report-design/references/layouts/marketing.md +45 -0
  42. package/skills/report-design/references/layouts/operations.md +44 -0
  43. package/skills/report-design/references/layouts/sales.md +50 -0
  44. package/skills/report-design/references/native-visuals.md +341 -0
  45. package/skills/report-design/references/pbi-desktop-installation.md +87 -0
  46. package/skills/report-design/references/pbir-preview-activation.md +40 -0
  47. package/skills/report-design/references/slicer.md +89 -0
  48. package/skills/report-design/references/textbox.md +101 -0
  49. package/skills/report-design/references/themes/BISuperpowers.json +915 -0
  50. package/skills/report-design/references/troubleshooting.md +135 -0
  51. package/skills/report-design/references/visual-types.md +78 -0
  52. package/skills/report-design/scripts/apply-theme.js +243 -0
  53. package/skills/report-design/scripts/create-visual.js +878 -0
  54. package/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
  55. package/skills/report-design/scripts/update-check.js +403 -0
  56. package/skills/report-design/scripts/validate-pbir.js +322 -0
  57. package/src/content/base.md +12 -68
  58. package/src/content/mcp-requirements.json +0 -25
  59. package/src/content/routing.md +19 -74
  60. package/src/content/skills/bi-start.md +191 -0
  61. package/src/content/skills/pbi-connect.md +22 -65
  62. package/src/content/skills/project-kickoff.md +372 -673
  63. package/src/content/skills/report-design/SKILL.md +376 -0
  64. package/src/content/skills/report-design/references/cli-commands.md +184 -0
  65. package/src/content/skills/report-design/references/cli-setup.md +101 -0
  66. package/src/content/skills/report-design/references/close-write-open-pattern.md +80 -0
  67. package/src/content/skills/report-design/references/layouts/finance.md +65 -0
  68. package/src/content/skills/report-design/references/layouts/generic.md +46 -0
  69. package/src/content/skills/report-design/references/layouts/hr.md +48 -0
  70. package/src/content/skills/report-design/references/layouts/marketing.md +45 -0
  71. package/src/content/skills/report-design/references/layouts/operations.md +44 -0
  72. package/src/content/skills/report-design/references/layouts/sales.md +50 -0
  73. package/src/content/skills/report-design/references/native-visuals.md +341 -0
  74. package/src/content/skills/report-design/references/pbi-desktop-installation.md +87 -0
  75. package/src/content/skills/report-design/references/pbir-preview-activation.md +40 -0
  76. package/src/content/skills/report-design/references/slicer.md +89 -0
  77. package/src/content/skills/report-design/references/textbox.md +101 -0
  78. package/src/content/skills/report-design/references/themes/BISuperpowers.json +915 -0
  79. package/src/content/skills/report-design/references/troubleshooting.md +135 -0
  80. package/src/content/skills/report-design/references/visual-types.md +78 -0
  81. package/src/content/skills/report-design/scripts/apply-theme.js +243 -0
  82. package/src/content/skills/report-design/scripts/create-visual.js +878 -0
  83. package/src/content/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
  84. package/src/content/skills/report-design/scripts/validate-pbir.js +322 -0
  85. package/bin/commands/install.test.js +0 -289
  86. package/bin/commands/lint.test.js +0 -103
  87. package/bin/lib/generators/claude-plugin.test.js +0 -111
  88. package/bin/lib/mcp-config.test.js +0 -310
  89. package/bin/lib/microsoft-mcp.test.js +0 -115
  90. package/bin/utils/mcp-detect.test.js +0 -81
  91. package/bin/utils/tui.test.js +0 -127
@@ -1,103 +0,0 @@
1
- /**
2
- * Unit tests for the lint command (checkup)
3
- *
4
- * Run with: npm test
5
- */
6
-
7
- const { test, describe } = require('node:test');
8
- const assert = require('node:assert');
9
- const fs = require('fs');
10
- const path = require('path');
11
-
12
- // Mock skill content for testing
13
- const validSkillContent = `# Test Skill
14
-
15
- ## Trigger
16
- Activate this skill when user mentions:
17
- - "test keyword"
18
- - "another trigger"
19
-
20
- ## Identity
21
- You are a **Test Expert** who helps users with testing.
22
-
23
- ## MANDATORY RULES
24
- 1. **ALWAYS TEST FIRST.** Never skip validation.
25
- 2. **USE ASSERTIONS.** Verify expected outcomes.
26
-
27
- ---
28
-
29
- ## PHASE 0: Initial Assessment
30
-
31
- Start with basic questions.
32
-
33
- \`\`\`dax
34
- // Example DAX
35
- TestMeasure = SUM(Table[Column])
36
- \`\`\`
37
- `;
38
-
39
- const invalidSkillContent = `# Missing Sections
40
-
41
- This skill is missing required sections like Trigger and MANDATORY RULES.
42
-
43
- Some content here but no proper structure.
44
- `;
45
-
46
- describe('Lint Command', () => {
47
- test('valid skill content should have required sections', () => {
48
- const hasTrigger = /##\s+Trigger/i.test(validSkillContent);
49
- const hasIdentity = /##\s+Identity/i.test(validSkillContent);
50
- const hasMandatoryRules = /##\s+MANDATORY RULES/i.test(validSkillContent);
51
-
52
- assert.strictEqual(hasTrigger, true, 'Should have Trigger section');
53
- assert.strictEqual(hasIdentity, true, 'Should have Identity section');
54
- assert.strictEqual(hasMandatoryRules, true, 'Should have MANDATORY RULES section');
55
- });
56
-
57
- test('invalid skill content should be missing required sections', () => {
58
- const hasTrigger = /##\s+Trigger/i.test(invalidSkillContent);
59
- const hasMandatoryRules = /##\s+MANDATORY RULES/i.test(invalidSkillContent);
60
-
61
- assert.strictEqual(hasTrigger, false, 'Should not have Trigger section');
62
- assert.strictEqual(hasMandatoryRules, false, 'Should not have MANDATORY RULES section');
63
- });
64
-
65
- test('skill should have H1 title', () => {
66
- const hasH1 = /^#\s+.+/m.test(validSkillContent);
67
- assert.strictEqual(hasH1, true, 'Should have H1 title');
68
- });
69
-
70
- test('code blocks should have language specifier', () => {
71
- // Check for code blocks with language
72
- const codeBlockWithLang = /```\w+/;
73
- const hasLanguage = codeBlockWithLang.test(validSkillContent);
74
- assert.strictEqual(hasLanguage, true, 'Code blocks should have language specifier');
75
- });
76
-
77
- test('trigger section should have quoted phrases', () => {
78
- const triggerSection = validSkillContent.match(/##\s+Trigger[\s\S]*?(?=##|$)/i);
79
- assert.ok(triggerSection, 'Should have Trigger section');
80
-
81
- const hasQuotedPhrases = /[-*]\s+["'].+["']/.test(triggerSection[0]);
82
- assert.strictEqual(hasQuotedPhrases, true, 'Trigger should have quoted phrases');
83
- });
84
- });
85
-
86
- describe('File naming convention', () => {
87
- test('kebab-case pattern should match valid names', () => {
88
- const pattern = /^[a-z0-9-]+\.md$/;
89
-
90
- assert.strictEqual(pattern.test('dax.md'), true);
91
- assert.strictEqual(pattern.test('power-query.md'), true);
92
- assert.strictEqual(pattern.test('data-model-design.md'), true);
93
- });
94
-
95
- test('kebab-case pattern should reject invalid names', () => {
96
- const pattern = /^[a-z0-9-]+\.md$/;
97
-
98
- assert.strictEqual(pattern.test('DAX.md'), false);
99
- assert.strictEqual(pattern.test('PowerQuery.md'), false);
100
- assert.strictEqual(pattern.test('my_skill.md'), false);
101
- assert.strictEqual(pattern.test('skill.txt'), false);
102
- });
103
- });
@@ -1,111 +0,0 @@
1
- /**
2
- * Tests for the native Claude Code plugin generator.
3
- */
4
-
5
- const { test, describe } = 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 claudePlugin = require('./claude-plugin');
12
-
13
- function makeSkill(name, title) {
14
- return {
15
- name,
16
- path: `/virtual/${name}.md`,
17
- content: `# ${title}
18
-
19
- ## Trigger
20
- - "${name}"
21
-
22
- ## Identity
23
- You are the ${title} guide.
24
-
25
- ## MANDATORY RULES
26
- 1. Stay helpful.
27
-
28
- Reference: library/snippets/example.md
29
- `,
30
- };
31
- }
32
-
33
- describe('claude-plugin generator', () => {
34
- test('creates plugin manifest, mcp config, commands, and skills', async () => {
35
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bi-superpowers-plugin-'));
36
-
37
- try {
38
- const skills = [
39
- makeSkill('project-kickoff', 'Project Kickoff'),
40
- makeSkill('pbi-connect', 'PBI Connect'),
41
- ];
42
-
43
- await claudePlugin.generate(tempDir, skills, {
44
- packageDir: '/tmp/bi-superpowers',
45
- version: '9.9.9',
46
- usePluginRootLauncher: true,
47
- libraryPrefix: 'library',
48
- });
49
-
50
- const pluginManifestPath = path.join(tempDir, '.claude-plugin', 'plugin.json');
51
- const pluginMcpPath = path.join(tempDir, '.mcp.json');
52
- const commandPath = path.join(tempDir, 'commands', 'project-kickoff.md');
53
- const skillPath = path.join(tempDir, 'skills', 'pbi-connect', 'SKILL.md');
54
-
55
- assert.ok(fs.existsSync(pluginManifestPath));
56
- assert.ok(fs.existsSync(pluginMcpPath));
57
- assert.ok(fs.existsSync(commandPath));
58
- assert.ok(fs.existsSync(skillPath));
59
-
60
- const pluginManifest = JSON.parse(fs.readFileSync(pluginManifestPath, 'utf8'));
61
- const pluginMcp = JSON.parse(fs.readFileSync(pluginMcpPath, 'utf8'));
62
- const commandContent = fs.readFileSync(commandPath, 'utf8');
63
- const skillContent = fs.readFileSync(skillPath, 'utf8');
64
-
65
- assert.strictEqual(pluginManifest.name, 'bi-superpowers');
66
- assert.strictEqual(pluginManifest.version, '9.9.9');
67
-
68
- // Only 2 MCPs ship: local Power BI Modeling and Microsoft Learn
69
- assert.strictEqual(Object.keys(pluginMcp).length, 2);
70
- assert.ok(pluginMcp['powerbi-modeling-mcp']);
71
- assert.ok(pluginMcp['microsoft-learn']);
72
- assert.strictEqual(pluginMcp['powerbi-modeling-mcp'].command, 'node');
73
- assert.ok(
74
- pluginMcp['powerbi-modeling-mcp'].args[0].includes(
75
- '${CLAUDE_PLUGIN_ROOT}/bin/mcp/powerbi-modeling-launcher.js'
76
- )
77
- );
78
- assert.strictEqual(pluginMcp['microsoft-learn'].type, 'http');
79
- assert.strictEqual(pluginMcp['microsoft-learn'].url, 'https://learn.microsoft.com/api/mcp');
80
-
81
- assert.ok(commandContent.includes('Generated by BI Agent Superpowers'));
82
- assert.ok(skillContent.includes('name: "pbi-connect"'));
83
- assert.ok(skillContent.includes('version: "9.9.9"'));
84
- } finally {
85
- fs.rmSync(tempDir, { recursive: true, force: true });
86
- }
87
- });
88
-
89
- test('rewrites library references for generated project outputs', async () => {
90
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bi-superpowers-plugin-'));
91
-
92
- try {
93
- const skills = [makeSkill('pbi-connect', 'PBI Connect')];
94
-
95
- await claudePlugin.generate(tempDir, skills, {
96
- packageDir: '/tmp/bi-superpowers',
97
- version: '1.0.0',
98
- libraryPrefix: '.bi-superpowers/library',
99
- });
100
-
101
- const skillContent = fs.readFileSync(
102
- path.join(tempDir, 'skills', 'pbi-connect', 'SKILL.md'),
103
- 'utf8'
104
- );
105
-
106
- assert.ok(skillContent.includes('.bi-superpowers/library/snippets/example.md'));
107
- } finally {
108
- fs.rmSync(tempDir, { recursive: true, force: true });
109
- }
110
- });
111
- });
@@ -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
- });