@luquimbo/bi-superpowers 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +8 -0
- package/.mcp.json +25 -0
- package/AGENTS.md +244 -0
- package/CHANGELOG.md +265 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/bin/build-plugin.js +30 -0
- package/bin/cli.js +1064 -0
- package/bin/commands/add.js +533 -0
- package/bin/commands/add.test.js +77 -0
- package/bin/commands/build-desktop.js +166 -0
- package/bin/commands/changelog.js +443 -0
- package/bin/commands/diff.js +325 -0
- package/bin/commands/lint.js +419 -0
- package/bin/commands/lint.test.js +103 -0
- package/bin/commands/mcp-setup.js +246 -0
- package/bin/commands/pull.js +287 -0
- package/bin/commands/pull.test.js +36 -0
- package/bin/commands/push.js +231 -0
- package/bin/commands/push.test.js +14 -0
- package/bin/commands/search.js +344 -0
- package/bin/commands/search.test.js +115 -0
- package/bin/commands/setup.js +545 -0
- package/bin/commands/setup.test.js +46 -0
- package/bin/commands/sync-profile.js +405 -0
- package/bin/commands/sync-profile.test.js +14 -0
- package/bin/commands/sync-source.js +418 -0
- package/bin/commands/sync-source.test.js +14 -0
- package/bin/commands/watch.js +206 -0
- package/bin/lib/generators/claude-plugin.js +266 -0
- package/bin/lib/generators/claude-plugin.test.js +110 -0
- package/bin/lib/generators/index.js +116 -0
- package/bin/lib/generators/shared.js +282 -0
- package/bin/lib/licensing/index.js +35 -0
- package/bin/lib/licensing/storage.js +364 -0
- package/bin/lib/licensing/storage.test.js +55 -0
- package/bin/lib/licensing/validator.js +213 -0
- package/bin/lib/licensing/validator.test.js +137 -0
- package/bin/lib/microsoft-mcp.js +176 -0
- package/bin/lib/microsoft-mcp.test.js +106 -0
- package/bin/lib/skills.js +84 -0
- package/bin/mcp/powerbi-modeling-launcher.js +38 -0
- package/bin/postinstall.js +44 -0
- package/bin/utils/errors.js +159 -0
- package/bin/utils/git.js +298 -0
- package/bin/utils/logger.js +142 -0
- package/bin/utils/mcp-detect.js +274 -0
- package/bin/utils/mcp-detect.test.js +105 -0
- package/bin/utils/pbix.js +305 -0
- package/bin/utils/pbix.test.js +37 -0
- package/bin/utils/profiles.js +312 -0
- package/bin/utils/projects.js +168 -0
- package/bin/utils/readline.js +206 -0
- package/bin/utils/readline.test.js +47 -0
- package/bin/utils/tui.js +314 -0
- package/bin/utils/tui.test.js +127 -0
- package/commands/contributions.md +265 -0
- package/commands/data-model-design.md +468 -0
- package/commands/dax-doctor.md +248 -0
- package/commands/fabric-scripts.md +452 -0
- package/commands/migration-assistant.md +290 -0
- package/commands/model-documenter.md +242 -0
- package/commands/pbi-connect.md +239 -0
- package/commands/project-kickoff.md +905 -0
- package/commands/report-layout.md +296 -0
- package/commands/rls-design.md +533 -0
- package/commands/theme-tweaker.md +624 -0
- package/config.example.json +23 -0
- package/config.json +23 -0
- package/desktop-extension/manifest.json +37 -0
- package/desktop-extension/package.json +10 -0
- package/desktop-extension/server.js +95 -0
- package/docs/openrouter-free-models.md +92 -0
- package/library/examples/README.md +151 -0
- package/library/examples/finance-reporting/README.md +351 -0
- package/library/examples/finance-reporting/data-model.md +267 -0
- package/library/examples/finance-reporting/measures.dax +557 -0
- package/library/examples/hr-analytics/README.md +371 -0
- package/library/examples/hr-analytics/data-model.md +315 -0
- package/library/examples/hr-analytics/measures.dax +460 -0
- package/library/examples/marketing-analytics/README.md +37 -0
- package/library/examples/marketing-analytics/data-model.md +62 -0
- package/library/examples/marketing-analytics/measures.dax +110 -0
- package/library/examples/retail-analytics/README.md +439 -0
- package/library/examples/retail-analytics/data-model.md +288 -0
- package/library/examples/retail-analytics/measures.dax +481 -0
- package/library/examples/supply-chain/README.md +37 -0
- package/library/examples/supply-chain/data-model.md +69 -0
- package/library/examples/supply-chain/measures.dax +77 -0
- package/library/examples/udf-library/README.md +228 -0
- package/library/examples/udf-library/functions.dax +571 -0
- package/library/snippets/dax/README.md +292 -0
- package/library/snippets/dax/business-domains.md +576 -0
- package/library/snippets/dax/calculate-patterns.md +276 -0
- package/library/snippets/dax/calculation-groups.md +489 -0
- package/library/snippets/dax/error-handling.md +495 -0
- package/library/snippets/dax/iterators-and-aggregations.md +474 -0
- package/library/snippets/dax/kpis-and-metrics.md +293 -0
- package/library/snippets/dax/rankings-and-topn.md +235 -0
- package/library/snippets/dax/security-patterns.md +413 -0
- package/library/snippets/dax/text-and-formatting.md +316 -0
- package/library/snippets/dax/time-intelligence.md +196 -0
- package/library/snippets/dax/user-defined-functions.md +477 -0
- package/library/snippets/dax/virtual-tables.md +546 -0
- package/library/snippets/excel-formulas/README.md +84 -0
- package/library/snippets/excel-formulas/aggregations.md +330 -0
- package/library/snippets/excel-formulas/dates-and-times.md +361 -0
- package/library/snippets/excel-formulas/dynamic-arrays.md +314 -0
- package/library/snippets/excel-formulas/lookups.md +169 -0
- package/library/snippets/excel-formulas/text-functions.md +363 -0
- package/library/snippets/governance/naming-conventions.md +97 -0
- package/library/snippets/governance/review-checklists.md +107 -0
- package/library/snippets/power-query/README.md +389 -0
- package/library/snippets/power-query/api-integration.md +707 -0
- package/library/snippets/power-query/connections.md +434 -0
- package/library/snippets/power-query/data-cleaning.md +298 -0
- package/library/snippets/power-query/error-handling.md +526 -0
- package/library/snippets/power-query/parameters.md +350 -0
- package/library/snippets/power-query/performance.md +506 -0
- package/library/snippets/power-query/transformations.md +330 -0
- package/library/snippets/report-design/accessibility.md +78 -0
- package/library/snippets/report-design/chart-selection.md +54 -0
- package/library/snippets/report-design/layout-patterns.md +87 -0
- package/library/templates/data-models/README.md +93 -0
- package/library/templates/data-models/finance-model.md +627 -0
- package/library/templates/data-models/retail-star-schema.md +473 -0
- package/library/templates/excel/README.md +83 -0
- package/library/templates/excel/budget-tracker.md +432 -0
- package/library/templates/excel/data-entry-form.md +533 -0
- package/library/templates/power-bi/README.md +72 -0
- package/library/templates/power-bi/finance-report.md +449 -0
- package/library/templates/power-bi/kpi-scorecard.md +461 -0
- package/library/templates/power-bi/sales-dashboard.md +281 -0
- package/library/themes/excel/README.md +436 -0
- package/library/themes/power-bi/README.md +271 -0
- package/library/themes/power-bi/accessible.json +307 -0
- package/library/themes/power-bi/bi-superpowers-default.json +858 -0
- package/library/themes/power-bi/corporate-blue.json +291 -0
- package/library/themes/power-bi/dark-mode.json +291 -0
- package/library/themes/power-bi/minimal.json +292 -0
- package/library/themes/power-bi/print-friendly.json +309 -0
- package/package.json +93 -0
- package/skills/contributions/SKILL.md +267 -0
- package/skills/data-model-design/SKILL.md +470 -0
- package/skills/data-modeling/SKILL.md +254 -0
- package/skills/data-quality/SKILL.md +664 -0
- package/skills/dax/SKILL.md +708 -0
- package/skills/dax-doctor/SKILL.md +250 -0
- package/skills/dax-udf/SKILL.md +489 -0
- package/skills/deployment/SKILL.md +320 -0
- package/skills/excel-formulas/SKILL.md +463 -0
- package/skills/fabric-scripts/SKILL.md +454 -0
- package/skills/fast-standard/SKILL.md +509 -0
- package/skills/governance/SKILL.md +205 -0
- package/skills/migration-assistant/SKILL.md +292 -0
- package/skills/model-documenter/SKILL.md +244 -0
- package/skills/pbi-connect/SKILL.md +241 -0
- package/skills/power-query/SKILL.md +406 -0
- package/skills/project-kickoff/SKILL.md +907 -0
- package/skills/query-performance/SKILL.md +480 -0
- package/skills/report-design/SKILL.md +207 -0
- package/skills/report-layout/SKILL.md +298 -0
- package/skills/rls-design/SKILL.md +535 -0
- package/skills/semantic-model/SKILL.md +237 -0
- package/skills/testing-validation/SKILL.md +643 -0
- package/skills/theme-tweaker/SKILL.md +626 -0
- package/src/content/base.md +237 -0
- package/src/content/mcp-requirements.json +69 -0
- package/src/content/routing.md +203 -0
- package/src/content/skills/contributions.md +259 -0
- package/src/content/skills/data-model-design.md +462 -0
- package/src/content/skills/data-modeling.md +246 -0
- package/src/content/skills/data-quality.md +656 -0
- package/src/content/skills/dax-doctor.md +242 -0
- package/src/content/skills/dax-udf.md +481 -0
- package/src/content/skills/dax.md +700 -0
- package/src/content/skills/deployment.md +312 -0
- package/src/content/skills/excel-formulas.md +455 -0
- package/src/content/skills/fabric-scripts.md +446 -0
- package/src/content/skills/fast-standard.md +501 -0
- package/src/content/skills/governance.md +197 -0
- package/src/content/skills/migration-assistant.md +284 -0
- package/src/content/skills/model-documenter.md +236 -0
- package/src/content/skills/pbi-connect.md +233 -0
- package/src/content/skills/power-query.md +398 -0
- package/src/content/skills/project-kickoff.md +899 -0
- package/src/content/skills/query-performance.md +472 -0
- package/src/content/skills/report-design.md +199 -0
- package/src/content/skills/report-layout.md +290 -0
- package/src/content/skills/rls-design.md +527 -0
- package/src/content/skills/semantic-model.md +229 -0
- package/src/content/skills/testing-validation.md +635 -0
- package/src/content/skills/theme-tweaker.md +618 -0
|
@@ -0,0 +1,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
|
+
};
|