@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,282 @@
1
+ /**
2
+ * Shared Generator Functions
3
+ * ==========================
4
+ *
5
+ * Common functions used by all AI tool generators.
6
+ *
7
+ * @module lib/generators/shared
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ /**
14
+ * Extract skill metadata from markdown content
15
+ *
16
+ * Parses a skill file to extract:
17
+ * - title: From the first H1 heading
18
+ * - triggers: Array of activation phrases from the Trigger section
19
+ * - identity: The AI role description from the Identity section
20
+ *
21
+ * @param {string} content - Raw markdown content of skill file
22
+ * @returns {Object} Parsed metadata
23
+ */
24
+ function parseSkillMetadata(content) {
25
+ const metadata = {
26
+ title: '',
27
+ triggers: [],
28
+ identity: '',
29
+ };
30
+
31
+ // Get title from first H1
32
+ const titleMatch = content.match(/^#\s+(.+)/m);
33
+ if (titleMatch) metadata.title = titleMatch[1];
34
+
35
+ // Get triggers
36
+ const triggerSection = content.match(/##\s+Trigger[\s\S]*?(?=##|$)/i);
37
+ if (triggerSection) {
38
+ const triggers = triggerSection[0].match(/[-*]\s+"([^"]+)"/g);
39
+ if (triggers) {
40
+ metadata.triggers = triggers.map((t) => t.replace(/[-*]\s+"/, '').replace('"', ''));
41
+ }
42
+ }
43
+
44
+ // Get identity
45
+ const identityMatch = content.match(/##\s+Identity[\s\S]*?(?=##|---)/i);
46
+ if (identityMatch) {
47
+ metadata.identity = identityMatch[0].replace(/##\s+Identity\s*/i, '').trim();
48
+ }
49
+
50
+ return metadata;
51
+ }
52
+
53
+ /**
54
+ * Get skill purpose description for table
55
+ * @param {string} skillName - Name of the skill
56
+ * @returns {string} Purpose description
57
+ */
58
+ function getSkillPurpose(skillName) {
59
+ const purposes = {
60
+ dax: 'DAX writing and optimization',
61
+ 'power-query': 'Power Query / M language patterns',
62
+ 'data-modeling': 'Star schema design',
63
+ 'data-model-design': 'Interactive model builder',
64
+ 'excel-formulas': 'Modern Excel 365 formulas',
65
+ 'project-kickoff': 'Project analysis and planning',
66
+ 'theme-tweaker': 'Power BI theme customization',
67
+ 'rls-design': 'Row-level security design',
68
+ 'query-performance': 'Performance optimization',
69
+ 'data-quality': 'Data validation and profiling',
70
+ 'fabric-scripts': 'Fabric automation scripts',
71
+ 'fast-standard': 'FAST spreadsheet standard',
72
+ 'testing-validation': 'Testing and validation patterns',
73
+ 'pbi-connect': 'Power BI Desktop connection',
74
+ contributions: 'Contribution validation',
75
+ // New command skills
76
+ 'dax-doctor': 'DAX debugging and optimization wizard',
77
+ 'model-documenter': 'Semantic model documentation generator',
78
+ 'migration-assistant': 'Migration and upgrade assistant',
79
+ 'report-layout': 'Report page layout planner',
80
+ // New reference skills
81
+ governance: 'Naming conventions, standards, and governance',
82
+ 'semantic-model': 'Semantic model best practices and patterns',
83
+ 'report-design': 'Report design and visualization principles',
84
+ deployment: 'CI/CD and deployment patterns for BI',
85
+ 'dax-udf': 'DAX user-defined functions (UDFs)',
86
+ };
87
+ return purposes[skillName] || 'Specialized BI assistance';
88
+ }
89
+
90
+ /**
91
+ * Generate skills summary table for combined configs
92
+ * @param {Object[]} skills - Array of skill objects
93
+ * @returns {string} Markdown table
94
+ */
95
+ function generateSkillsSection(skills) {
96
+ let section = `---
97
+
98
+ ## Available Skills
99
+
100
+ When you detect these triggers, apply the corresponding skill knowledge:
101
+
102
+ | Skill | Triggers | Purpose |
103
+ |-------|----------|---------|
104
+ `;
105
+
106
+ for (const skill of skills) {
107
+ const meta = parseSkillMetadata(skill.content);
108
+ const triggers =
109
+ meta.triggers
110
+ .slice(0, 3)
111
+ .map((t) => `"${t}"`)
112
+ .join(', ') || `"${skill.name}"`;
113
+ section += `| **${meta.title || skill.name}** | ${triggers} | ${getSkillPurpose(skill.name)} |\n`;
114
+ }
115
+
116
+ return section;
117
+ }
118
+
119
+ /**
120
+ * Get code standards section for combined configs
121
+ * @returns {string} Markdown code standards
122
+ */
123
+ function getCodeStandards() {
124
+ return `---
125
+
126
+ ## Code Standards
127
+
128
+ ### DAX
129
+
130
+ \`\`\`dax
131
+ -- Use VAR/RETURN for clarity and performance
132
+ SalesYoY =
133
+ VAR _Current = [TotalSales]
134
+ VAR _Prior = CALCULATE([TotalSales], SAMEPERIODLASTYEAR('Date'[Date]))
135
+ RETURN DIVIDE(_Current - _Prior, _Prior)
136
+
137
+ -- Always use DIVIDE for safe division
138
+ Margin = DIVIDE([Profit], [Revenue], 0)
139
+
140
+ -- Time intelligence
141
+ Sales_YTD = TOTALYTD([TotalSales], 'Date'[Date])
142
+ Sales_PY = CALCULATE([TotalSales], SAMEPERIODLASTYEAR('Date'[Date]))
143
+ \`\`\`
144
+
145
+ ### Power Query
146
+
147
+ \`\`\`powerquery
148
+ let
149
+ Source = Sql.Database("server", "database"),
150
+ Filtered = Table.SelectRows(Source, each [Status] = "Active"),
151
+ Typed = Table.TransformColumnTypes(Filtered, {{"Date", type date}})
152
+ in
153
+ Typed
154
+ \`\`\`
155
+
156
+ ### Excel
157
+
158
+ \`\`\`excel
159
+ =XLOOKUP(A1, Table[ID], Table[Name], "Not Found")
160
+ =FILTER(Data, Data[Status]="Active")
161
+ =LET(x, SUM(A:A), y, COUNT(A:A), DIVIDE(x, y))
162
+ \`\`\`
163
+
164
+ ### Naming Conventions
165
+
166
+ | Element | Convention | Example |
167
+ |---------|------------|---------|
168
+ | Measures | PascalCase | \`TotalSales\` |
169
+ | Tables | Singular | \`Customer\` |
170
+ | Columns | PascalCase | \`OrderDate\` |
171
+ | Variables | Underscore prefix | \`_Result\` |`;
172
+ }
173
+
174
+ /**
175
+ * Get format-specific header
176
+ * @param {string} format - Format type (cursor, copilot, chatgpt, generic)
177
+ * @param {number} skillCount - Number of skills
178
+ * @returns {string} Header content
179
+ */
180
+ function getFormatHeader(format, skillCount) {
181
+ const headers = {
182
+ cursor: `# BI Agent Superpowers - Cursor Rules
183
+
184
+ You are a Business Intelligence Expert with deep knowledge of Power BI, Microsoft Fabric, and Excel.
185
+ Generated automatically from ${skillCount} skill definitions.`,
186
+
187
+ copilot: `# BI Agent Superpowers - GitHub Copilot Instructions
188
+
189
+ You are a Business Intelligence Expert specializing in Power BI, Microsoft Fabric, and Excel development.
190
+ Generated automatically from ${skillCount} skill definitions.`,
191
+
192
+ chatgpt: `# BI Agent Superpowers - ChatGPT Custom Instructions
193
+
194
+ Copy this content to ChatGPT > Settings > Personalization > Custom Instructions.
195
+
196
+ You are a Business Intelligence Expert specializing in Power BI, Microsoft Fabric, and Excel.`,
197
+
198
+ generic: `# BI Agent Superpowers - AI Context
199
+
200
+ Universal context for any AI assistant working with Business Intelligence development.
201
+ Generated automatically from ${skillCount} skill definitions.`,
202
+ };
203
+ return headers[format] || headers.generic;
204
+ }
205
+
206
+ /**
207
+ * Get format-specific footer
208
+ * @returns {string} Footer content
209
+ */
210
+ function getFormatFooter() {
211
+ return `---
212
+
213
+ ## Resources
214
+
215
+ - Library: \`library/snippets/\` - DAX, Power Query, Excel patterns
216
+ - Themes: \`library/themes/power-bi/\` - JSON theme files
217
+ - Examples: \`library/examples/\` - Reference implementations
218
+
219
+ ---
220
+
221
+ **BI Agent Superpowers** - Developed by Lucas Sanchez (@luquimbo)
222
+ https://github.com/luquimbo/bi-superpowers`;
223
+ }
224
+
225
+ /**
226
+ * Generate combined configuration for tools that use a single file
227
+ * @param {Object[]} skills - Array of skill objects
228
+ * @param {string} format - Format type (cursor, copilot, chatgpt, generic)
229
+ * @returns {string} Combined markdown content
230
+ */
231
+ function generateCombinedConfig(skills, format) {
232
+ const header = getFormatHeader(format, skills.length);
233
+ const skillsSection = generateSkillsSection(skills);
234
+ const codeStandards = getCodeStandards();
235
+ const footer = getFormatFooter();
236
+
237
+ return `${header}
238
+
239
+ ${skillsSection}
240
+
241
+ ${codeStandards}
242
+
243
+ ${footer}`;
244
+ }
245
+
246
+ /**
247
+ * Create symlink to content cache directory for easy access to library
248
+ * @param {string} targetDir - Project directory path
249
+ * @param {string} contentCacheDir - Content cache directory path
250
+ * @param {string} symlinkName - Name of the symlink to create
251
+ */
252
+ function createSymlink(targetDir, contentCacheDir, symlinkName) {
253
+ const linkPath = path.join(targetDir, symlinkName);
254
+ // Remove existing symlink if it exists
255
+ if (fs.existsSync(linkPath)) {
256
+ try {
257
+ fs.unlinkSync(linkPath);
258
+ } catch (e) {
259
+ // Ignore errors
260
+ }
261
+ }
262
+ try {
263
+ fs.symlinkSync(contentCacheDir, linkPath, 'dir');
264
+ console.log(` ✓ Created ${symlinkName} symlink`);
265
+ } catch (e) {
266
+ // Symlink may fail on Windows without admin rights
267
+ if (process.env.DEBUG === 'true') {
268
+ console.error(`[DEBUG] Failed to create symlink: ${e.message}`);
269
+ }
270
+ }
271
+ }
272
+
273
+ module.exports = {
274
+ parseSkillMetadata,
275
+ getSkillPurpose,
276
+ generateSkillsSection,
277
+ getCodeStandards,
278
+ getFormatHeader,
279
+ getFormatFooter,
280
+ generateCombinedConfig,
281
+ createSymlink,
282
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Licensing Module
3
+ * =================
4
+ *
5
+ * Unified exports for all licensing-related functionality.
6
+ *
7
+ * @module lib/licensing
8
+ */
9
+
10
+ const storage = require('./storage');
11
+ const validator = require('./validator');
12
+
13
+ module.exports = {
14
+ // Storage functions
15
+ loadLicense: storage.loadLicense,
16
+ saveLicense: storage.saveLicense,
17
+ clearLicense: storage.clearLicense,
18
+ isContentInstalled: storage.isContentInstalled,
19
+ downloadPremiumContent: storage.downloadPremiumContent,
20
+ getContentCacheDir: storage.getContentCacheDir,
21
+ getSkillsDir: storage.getSkillsDir,
22
+ getLicenseFile: storage.getLicenseFile,
23
+
24
+ // Validator functions
25
+ makeRequest: validator.makeRequest,
26
+ validateLicense: validator.validateLicense,
27
+ requireLicense: validator.requireLicense,
28
+ getApiBaseUrl: validator.getApiBaseUrl,
29
+
30
+ // Constants
31
+ LICENSE_FILE: storage.LICENSE_FILE,
32
+ CONTENT_CACHE_DIR: storage.CONTENT_CACHE_DIR,
33
+ SKILLS_DIR: storage.SKILLS_DIR,
34
+ API_BASE_URL: validator.API_BASE_URL,
35
+ };
@@ -0,0 +1,364 @@
1
+ /**
2
+ * License Storage Module
3
+ * ======================
4
+ *
5
+ * Handles license persistence and premium content management.
6
+ * Licenses are stored in ~/.bi-superpowers-license as JSON.
7
+ *
8
+ * @module lib/licensing/storage
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+ const https = require('https');
15
+ const AdmZip = require('adm-zip');
16
+
17
+ /** Path to the license file stored in user's home directory */
18
+ const LICENSE_FILE = path.join(os.homedir(), '.bi-superpowers-license');
19
+
20
+ /** Directory for cached premium content (skills, snippets, themes) */
21
+ const CONTENT_CACHE_DIR = path.join(os.homedir(), '.bi-superpowers');
22
+
23
+ /** Directory containing skill definition files (.md) */
24
+ const SKILLS_DIR = path.join(CONTENT_CACHE_DIR, '.agents', 'prompts', 'skills');
25
+
26
+ /**
27
+ * Load saved license from disk
28
+ *
29
+ * Reads the license file from the user's home directory. The license contains:
30
+ * - license: The license key string
31
+ * - email: User's email address
32
+ * - name: User's name (optional)
33
+ * - activatedAt: ISO timestamp of activation
34
+ * - contentVersion: Version when content was last downloaded
35
+ *
36
+ * @returns {Object|null} License data object or null if not found/invalid
37
+ */
38
+ function loadLicense() {
39
+ try {
40
+ if (fs.existsSync(LICENSE_FILE)) {
41
+ const data = JSON.parse(fs.readFileSync(LICENSE_FILE, 'utf8'));
42
+ return data;
43
+ }
44
+ } catch (e) {
45
+ if (process.env.DEBUG === 'true') {
46
+ console.error(`[DEBUG] Failed to load license: ${e.message}`);
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Save license to disk
54
+ *
55
+ * Persists license data to ~/.bi-superpowers-license for future sessions.
56
+ *
57
+ * @param {Object} data - License data to save
58
+ * @param {string} data.license - The license key
59
+ * @param {string} data.email - User's email
60
+ * @param {string} [data.name] - User's name
61
+ * @param {string} data.activatedAt - Activation timestamp
62
+ * @param {string} data.contentVersion - Content version
63
+ */
64
+ function saveLicense(data) {
65
+ fs.writeFileSync(LICENSE_FILE, JSON.stringify(data, null, 2));
66
+ }
67
+
68
+ /**
69
+ * Clear saved license from disk
70
+ *
71
+ * Removes the license file, effectively logging out the user.
72
+ * Used when a license becomes invalid or user wants to switch accounts.
73
+ */
74
+ function clearLicense() {
75
+ try {
76
+ if (fs.existsSync(LICENSE_FILE)) {
77
+ fs.unlinkSync(LICENSE_FILE);
78
+ }
79
+ } catch (e) {
80
+ if (process.env.DEBUG === 'true') {
81
+ console.error(`[DEBUG] Failed to clear license: ${e.message}`);
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Check if premium content is installed
88
+ *
89
+ * Verifies that the skills directory exists and contains at least one .md file.
90
+ * Used to determine if content needs to be downloaded during license validation.
91
+ *
92
+ * @returns {boolean} True if skills directory exists with content
93
+ */
94
+ function isContentInstalled() {
95
+ return (
96
+ fs.existsSync(SKILLS_DIR) &&
97
+ fs.readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.md')).length > 0
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Download file from URL to local filesystem
103
+ *
104
+ * Streams the response directly to a file to handle large downloads efficiently.
105
+ * Automatically follows HTTP 301/302 redirects.
106
+ *
107
+ * @param {string} url - URL of the file to download
108
+ * @param {string} destPath - Local path where file will be saved
109
+ * @returns {Promise<void>} Resolves when download is complete
110
+ * @throws {Error} On network errors or non-200 status codes
111
+ */
112
+ function downloadFile(url, destPath) {
113
+ return downloadFileInternal(url, destPath, 0);
114
+ }
115
+
116
+ /**
117
+ * Resolve a redirect Location header to an absolute URL.
118
+ *
119
+ * @param {string|URL} currentUrl - The current request URL
120
+ * @param {string} location - The Location header value
121
+ * @returns {string} Absolute URL string
122
+ */
123
+ function resolveRedirectUrl(currentUrl, location) {
124
+ const baseUrl = currentUrl instanceof URL ? currentUrl : new URL(currentUrl);
125
+ return new URL(location, baseUrl).toString();
126
+ }
127
+
128
+ function safeUnlink(filePath) {
129
+ try {
130
+ fs.unlinkSync(filePath);
131
+ } catch (e) {
132
+ // Ignore missing file or cleanup failures
133
+ }
134
+ }
135
+
136
+ function downloadFileInternal(url, destPath, redirectCount) {
137
+ return new Promise((resolve, reject) => {
138
+ const urlObj = new URL(url);
139
+ if (urlObj.protocol !== 'https:') {
140
+ reject(new Error(`Insecure download URL (HTTPS required): ${url}`));
141
+ return;
142
+ }
143
+
144
+ const MAX_REDIRECTS = 5;
145
+ const TIMEOUT_MS = 30000;
146
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
147
+
148
+ const request = https.get(urlObj, (response) => {
149
+ const status = response.statusCode || 0;
150
+
151
+ // Handle redirects (including relative Location headers)
152
+ if (REDIRECT_STATUSES.has(status)) {
153
+ response.resume();
154
+
155
+ if (redirectCount >= MAX_REDIRECTS) {
156
+ reject(new Error(`Download failed: too many redirects (${MAX_REDIRECTS})`));
157
+ return;
158
+ }
159
+
160
+ const location = response.headers.location;
161
+ if (!location) {
162
+ reject(new Error(`Download failed: redirect (${status}) without Location header`));
163
+ return;
164
+ }
165
+
166
+ const nextUrl = resolveRedirectUrl(urlObj, location);
167
+ downloadFileInternal(nextUrl, destPath, redirectCount + 1)
168
+ .then(resolve)
169
+ .catch(reject);
170
+ return;
171
+ }
172
+
173
+ if (status !== 200) {
174
+ response.resume();
175
+ reject(new Error(`Download failed with status ${status}`));
176
+ return;
177
+ }
178
+
179
+ // Only create the output file once we know the response is successful.
180
+ const file = fs.createWriteStream(destPath);
181
+
182
+ file.on('error', (err) => {
183
+ response.destroy();
184
+ file.close(() => {
185
+ safeUnlink(destPath);
186
+ reject(err);
187
+ });
188
+ });
189
+
190
+ response.on('error', (err) => {
191
+ file.close(() => {
192
+ safeUnlink(destPath);
193
+ reject(err);
194
+ });
195
+ });
196
+
197
+ response.pipe(file);
198
+
199
+ file.on('finish', () => {
200
+ file.close(() => resolve());
201
+ });
202
+ });
203
+
204
+ request.setTimeout(TIMEOUT_MS, () => {
205
+ request.destroy(new Error(`Download request timed out after ${TIMEOUT_MS}ms`));
206
+ });
207
+
208
+ request.on('error', (err) => {
209
+ safeUnlink(destPath);
210
+ reject(err);
211
+ });
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Extract ZIP file to destination directory
217
+ * Uses native unzip command (macOS/Linux) or PowerShell (Windows)
218
+ *
219
+ * Security: Uses spawnSync with array arguments to prevent command injection
220
+ *
221
+ * @param {string} zipPath - Path to the ZIP file to extract
222
+ * @param {string} destDir - Destination directory for extraction
223
+ * @returns {Promise<void>} Resolves when extraction is complete
224
+ * @throws {Error} If extraction fails
225
+ */
226
+ function isUnsafeZipEntry(entryName, destRoot) {
227
+ if (!entryName) return true;
228
+
229
+ const normalized = entryName.replace(/\\/g, '/');
230
+
231
+ // Disallow absolute paths and Windows drive letters
232
+ if (normalized.startsWith('/') || normalized.startsWith('\\')) return true;
233
+ if (/^[A-Za-z]:/.test(normalized)) return true;
234
+ if (normalized.startsWith('//')) return true;
235
+
236
+ const targetPath = path.resolve(destRoot, entryName);
237
+ const relative = path.relative(destRoot, targetPath);
238
+
239
+ return relative.startsWith('..') || path.isAbsolute(relative);
240
+ }
241
+
242
+ function extractZip(zipPath, destDir) {
243
+ return new Promise((resolve, reject) => {
244
+ try {
245
+ // Ensure destination directory exists
246
+ if (!fs.existsSync(destDir)) {
247
+ fs.mkdirSync(destDir, { recursive: true });
248
+ }
249
+
250
+ const destRoot = path.resolve(destDir);
251
+ const zip = new AdmZip(zipPath);
252
+ const entries = zip.getEntries();
253
+
254
+ for (const entry of entries) {
255
+ if (isUnsafeZipEntry(entry.entryName, destRoot)) {
256
+ throw new Error(`Unsafe ZIP entry detected: ${entry.entryName}`);
257
+ }
258
+ }
259
+
260
+ zip.extractAllTo(destRoot, true);
261
+
262
+ resolve();
263
+ } catch (error) {
264
+ reject(new Error(`Failed to extract ZIP safely: ${error.message}`));
265
+ }
266
+ });
267
+ }
268
+
269
+ /**
270
+ * Download and install premium content from license server
271
+ *
272
+ * This function:
273
+ * 1. Creates the content cache directory if needed
274
+ * 2. Downloads a ZIP file containing skills, snippets, themes, etc.
275
+ * 3. Extracts the ZIP to ~/.bi-superpowers/
276
+ * 4. Cleans up the temporary ZIP file
277
+ * 5. Verifies the skills directory was created correctly
278
+ *
279
+ * @param {string} downloadUrl - URL provided by license validation API
280
+ * @returns {Promise<boolean>} True if download and extraction succeeded
281
+ */
282
+ async function downloadPremiumContent(downloadUrl) {
283
+ console.log('Downloading premium content...');
284
+
285
+ // Ensure cache directory exists
286
+ if (!fs.existsSync(CONTENT_CACHE_DIR)) {
287
+ fs.mkdirSync(CONTENT_CACHE_DIR, { recursive: true });
288
+ }
289
+
290
+ const zipPath = path.join(CONTENT_CACHE_DIR, 'content.zip');
291
+
292
+ try {
293
+ // Download the ZIP file
294
+ await downloadFile(downloadUrl, zipPath);
295
+ console.log('✓ Download complete');
296
+
297
+ // Extract the ZIP file
298
+ console.log('Extracting content...');
299
+ await extractZip(zipPath, CONTENT_CACHE_DIR);
300
+ console.log('✓ Extraction complete');
301
+
302
+ // Clean up ZIP file
303
+ fs.unlinkSync(zipPath);
304
+
305
+ // Verify skills directory exists
306
+ if (!fs.existsSync(SKILLS_DIR)) {
307
+ throw new Error('Skills directory not found after extraction');
308
+ }
309
+
310
+ const skillCount = fs.readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.md')).length;
311
+ console.log(`✓ ${skillCount} skills installed`);
312
+
313
+ return true;
314
+ } catch (error) {
315
+ console.error('Content download failed:', error.message);
316
+ // Clean up failed download
317
+ if (fs.existsSync(zipPath)) {
318
+ fs.unlinkSync(zipPath);
319
+ }
320
+ return false;
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Get the content cache directory path
326
+ * @returns {string} Path to content cache
327
+ */
328
+ function getContentCacheDir() {
329
+ return CONTENT_CACHE_DIR;
330
+ }
331
+
332
+ /**
333
+ * Get the skills directory path
334
+ * @returns {string} Path to skills directory
335
+ */
336
+ function getSkillsDir() {
337
+ return SKILLS_DIR;
338
+ }
339
+
340
+ /**
341
+ * Get the license file path
342
+ * @returns {string} Path to license file
343
+ */
344
+ function getLicenseFile() {
345
+ return LICENSE_FILE;
346
+ }
347
+
348
+ module.exports = {
349
+ loadLicense,
350
+ saveLicense,
351
+ clearLicense,
352
+ isContentInstalled,
353
+ downloadFile,
354
+ extractZip,
355
+ downloadPremiumContent,
356
+ getContentCacheDir,
357
+ getSkillsDir,
358
+ getLicenseFile,
359
+ _isUnsafeZipEntry: isUnsafeZipEntry,
360
+ _resolveRedirectUrl: resolveRedirectUrl,
361
+ LICENSE_FILE,
362
+ CONTENT_CACHE_DIR,
363
+ SKILLS_DIR,
364
+ };