@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,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lint Command (checkup)
|
|
3
|
+
* ======================
|
|
4
|
+
* Validates skill files for proper structure and format.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* super checkup Lint all skills
|
|
8
|
+
* super checkup dax.md Lint specific skill
|
|
9
|
+
* super checkup --fix Auto-fix issues (where possible)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const tui = require('../utils/tui');
|
|
15
|
+
|
|
16
|
+
// Lint rules configuration
|
|
17
|
+
const RULES = {
|
|
18
|
+
// Required sections
|
|
19
|
+
requiredSections: {
|
|
20
|
+
enabled: true,
|
|
21
|
+
severity: 'error',
|
|
22
|
+
sections: ['Trigger', 'Identity', 'MANDATORY RULES'],
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// Trigger section validation
|
|
26
|
+
triggerFormat: {
|
|
27
|
+
enabled: true,
|
|
28
|
+
severity: 'warning',
|
|
29
|
+
description: 'Trigger section should have bullet points with quoted phrases',
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Title validation
|
|
33
|
+
hasTitle: {
|
|
34
|
+
enabled: true,
|
|
35
|
+
severity: 'error',
|
|
36
|
+
description: 'Skill must have an H1 title',
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Code block validation
|
|
40
|
+
codeBlocks: {
|
|
41
|
+
enabled: true,
|
|
42
|
+
severity: 'warning',
|
|
43
|
+
description: 'Code blocks should have language specified',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Link validation
|
|
47
|
+
brokenLinks: {
|
|
48
|
+
enabled: true,
|
|
49
|
+
severity: 'warning',
|
|
50
|
+
description: 'Internal links should point to existing files',
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Maximum file size
|
|
54
|
+
maxSize: {
|
|
55
|
+
enabled: true,
|
|
56
|
+
severity: 'warning',
|
|
57
|
+
maxBytes: 50000, // 50KB
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// Naming convention
|
|
61
|
+
naming: {
|
|
62
|
+
enabled: true,
|
|
63
|
+
severity: 'error',
|
|
64
|
+
pattern: /^[a-z0-9-]+\.md$/,
|
|
65
|
+
description: 'Skill files should be lowercase-kebab-case.md',
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Lint a single skill file
|
|
71
|
+
* @param {string} filePath - Path to skill file
|
|
72
|
+
* @returns {Object} Lint result with errors and warnings
|
|
73
|
+
*/
|
|
74
|
+
function lintFile(filePath) {
|
|
75
|
+
const result = {
|
|
76
|
+
file: path.basename(filePath),
|
|
77
|
+
path: filePath,
|
|
78
|
+
errors: [],
|
|
79
|
+
warnings: [],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let content;
|
|
83
|
+
try {
|
|
84
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
85
|
+
} catch (e) {
|
|
86
|
+
result.errors.push({ rule: 'file-read', message: `Cannot read file: ${e.message}` });
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lines = content.split('\n');
|
|
91
|
+
const filename = path.basename(filePath);
|
|
92
|
+
|
|
93
|
+
// Rule: File naming convention
|
|
94
|
+
if (RULES.naming.enabled && !RULES.naming.pattern.test(filename)) {
|
|
95
|
+
addIssue(
|
|
96
|
+
result,
|
|
97
|
+
'naming',
|
|
98
|
+
RULES.naming.severity,
|
|
99
|
+
`File name "${filename}" should be lowercase-kebab-case.md`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Rule: Max file size
|
|
104
|
+
if (RULES.maxSize.enabled) {
|
|
105
|
+
const stats = fs.statSync(filePath);
|
|
106
|
+
if (stats.size > RULES.maxSize.maxBytes) {
|
|
107
|
+
addIssue(
|
|
108
|
+
result,
|
|
109
|
+
'maxSize',
|
|
110
|
+
RULES.maxSize.severity,
|
|
111
|
+
`File size (${(stats.size / 1024).toFixed(1)}KB) exceeds ${RULES.maxSize.maxBytes / 1024}KB limit`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Rule: Has H1 title
|
|
117
|
+
if (RULES.hasTitle.enabled) {
|
|
118
|
+
const hasH1 = lines.some((line) => /^#\s+.+/.test(line));
|
|
119
|
+
if (!hasH1) {
|
|
120
|
+
addIssue(
|
|
121
|
+
result,
|
|
122
|
+
'hasTitle',
|
|
123
|
+
RULES.hasTitle.severity,
|
|
124
|
+
'Missing H1 title (should start with "# Title")'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Rule: Required sections
|
|
130
|
+
if (RULES.requiredSections.enabled) {
|
|
131
|
+
RULES.requiredSections.sections.forEach((section) => {
|
|
132
|
+
const sectionPattern = new RegExp(`^##\\s+${section}`, 'mi');
|
|
133
|
+
if (!sectionPattern.test(content)) {
|
|
134
|
+
addIssue(
|
|
135
|
+
result,
|
|
136
|
+
'requiredSections',
|
|
137
|
+
RULES.requiredSections.severity,
|
|
138
|
+
`Missing required section: "## ${section}"`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Rule: Trigger format validation
|
|
145
|
+
if (RULES.triggerFormat.enabled) {
|
|
146
|
+
const triggerSection = extractSection(content, 'Trigger');
|
|
147
|
+
if (triggerSection) {
|
|
148
|
+
const hasBullets = /[-*]\s+["'].+["']/.test(triggerSection);
|
|
149
|
+
if (!hasBullets) {
|
|
150
|
+
const lineNum = findSectionLine(lines, 'Trigger');
|
|
151
|
+
addIssue(
|
|
152
|
+
result,
|
|
153
|
+
'triggerFormat',
|
|
154
|
+
RULES.triggerFormat.severity,
|
|
155
|
+
'Trigger section should have bullet points with quoted phrases (e.g., - "keyword")',
|
|
156
|
+
lineNum
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Rule: Code blocks should have language
|
|
163
|
+
if (RULES.codeBlocks.enabled) {
|
|
164
|
+
lines.forEach((line, index) => {
|
|
165
|
+
if (line.trim() === '```') {
|
|
166
|
+
// Check if this is an opening code fence without language
|
|
167
|
+
const nextLine = lines[index + 1] || '';
|
|
168
|
+
// It's a problem if it's not a closing fence and doesn't have a language
|
|
169
|
+
if (!nextLine.trim().startsWith('```')) {
|
|
170
|
+
addIssue(
|
|
171
|
+
result,
|
|
172
|
+
'codeBlocks',
|
|
173
|
+
RULES.codeBlocks.severity,
|
|
174
|
+
'Code block missing language specifier (e.g., ```dax)',
|
|
175
|
+
index + 1
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Rule: Check internal links
|
|
183
|
+
if (RULES.brokenLinks.enabled) {
|
|
184
|
+
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
185
|
+
let match;
|
|
186
|
+
while ((match = linkPattern.exec(content)) !== null) {
|
|
187
|
+
const linkPath = match[2];
|
|
188
|
+
// Only check relative markdown links
|
|
189
|
+
if (linkPath.endsWith('.md') && !linkPath.startsWith('http')) {
|
|
190
|
+
const resolvedPath = path.resolve(path.dirname(filePath), linkPath);
|
|
191
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
192
|
+
const lineNum = findLineWithText(lines, match[0]);
|
|
193
|
+
addIssue(
|
|
194
|
+
result,
|
|
195
|
+
'brokenLinks',
|
|
196
|
+
RULES.brokenLinks.severity,
|
|
197
|
+
`Broken internal link: ${linkPath}`,
|
|
198
|
+
lineNum
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Add an issue to the result
|
|
210
|
+
* @param {Object} result - Result object
|
|
211
|
+
* @param {string} rule - Rule name
|
|
212
|
+
* @param {string} severity - 'error' or 'warning'
|
|
213
|
+
* @param {string} message - Issue message
|
|
214
|
+
* @param {number} [line] - Line number
|
|
215
|
+
*/
|
|
216
|
+
function addIssue(result, rule, severity, message, line) {
|
|
217
|
+
const issue = { rule, message };
|
|
218
|
+
if (line) issue.line = line;
|
|
219
|
+
|
|
220
|
+
if (severity === 'error') {
|
|
221
|
+
result.errors.push(issue);
|
|
222
|
+
} else {
|
|
223
|
+
result.warnings.push(issue);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Extract a section from markdown content
|
|
229
|
+
* @param {string} content - Markdown content
|
|
230
|
+
* @param {string} sectionName - Section name (without ##)
|
|
231
|
+
* @returns {string|null} Section content or null
|
|
232
|
+
*/
|
|
233
|
+
function extractSection(content, sectionName) {
|
|
234
|
+
const pattern = new RegExp(`^##\\s+${sectionName}[\\s\\S]*?(?=^##|------|$)`, 'mi');
|
|
235
|
+
const match = content.match(pattern);
|
|
236
|
+
return match ? match[0] : null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Find line number of a section
|
|
241
|
+
* @param {string[]} lines - Array of lines
|
|
242
|
+
* @param {string} sectionName - Section name
|
|
243
|
+
* @returns {number|null} Line number or null
|
|
244
|
+
*/
|
|
245
|
+
function findSectionLine(lines, sectionName) {
|
|
246
|
+
for (let i = 0; i < lines.length; i++) {
|
|
247
|
+
if (new RegExp(`^##\\s+${sectionName}`, 'i').test(lines[i])) {
|
|
248
|
+
return i + 1;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Find line number containing text
|
|
256
|
+
* @param {string[]} lines - Array of lines
|
|
257
|
+
* @param {string} text - Text to find
|
|
258
|
+
* @returns {number|null} Line number or null
|
|
259
|
+
*/
|
|
260
|
+
function findLineWithText(lines, text) {
|
|
261
|
+
for (let i = 0; i < lines.length; i++) {
|
|
262
|
+
if (lines[i].includes(text)) {
|
|
263
|
+
return i + 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Parse command line arguments
|
|
271
|
+
* @param {string[]} args - CLI arguments
|
|
272
|
+
* @returns {Object} Parsed options
|
|
273
|
+
*/
|
|
274
|
+
function parseArgs(args) {
|
|
275
|
+
const options = {
|
|
276
|
+
files: [],
|
|
277
|
+
fix: false,
|
|
278
|
+
json: false,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
for (let i = 0; i < args.length; i++) {
|
|
282
|
+
const arg = args[i];
|
|
283
|
+
|
|
284
|
+
if (arg === '--fix') {
|
|
285
|
+
options.fix = true;
|
|
286
|
+
} else if (arg === '--json') {
|
|
287
|
+
options.json = true;
|
|
288
|
+
} else if (!arg.startsWith('-')) {
|
|
289
|
+
options.files.push(arg);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return options;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Display lint summary
|
|
298
|
+
* @param {Object[]} results - Array of lint results
|
|
299
|
+
*/
|
|
300
|
+
function displaySummary(results) {
|
|
301
|
+
const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
|
|
302
|
+
const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0);
|
|
303
|
+
const filesWithIssues = results.filter(
|
|
304
|
+
(r) => r.errors.length > 0 || r.warnings.length > 0
|
|
305
|
+
).length;
|
|
306
|
+
|
|
307
|
+
console.log('');
|
|
308
|
+
tui.section('Summary');
|
|
309
|
+
|
|
310
|
+
const table = tui.createTable(
|
|
311
|
+
['Metric', 'Count'],
|
|
312
|
+
[
|
|
313
|
+
['Files checked', results.length.toString()],
|
|
314
|
+
['Files with issues', filesWithIssues.toString()],
|
|
315
|
+
['Errors', tui.colors.error(totalErrors.toString())],
|
|
316
|
+
['Warnings', tui.colors.warning(totalWarnings.toString())],
|
|
317
|
+
]
|
|
318
|
+
);
|
|
319
|
+
console.log(table);
|
|
320
|
+
|
|
321
|
+
if (totalErrors > 0) {
|
|
322
|
+
console.log(`\n${tui.icons.error} ${tui.colors.error(`${totalErrors} error(s) found`)}`);
|
|
323
|
+
} else if (totalWarnings > 0) {
|
|
324
|
+
console.log(
|
|
325
|
+
`\n${tui.icons.warning} ${tui.colors.warning(`${totalWarnings} warning(s) found`)}`
|
|
326
|
+
);
|
|
327
|
+
} else {
|
|
328
|
+
console.log(`\n${tui.icons.success} ${tui.colors.success('All skills passed validation!')}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Main lint command handler
|
|
334
|
+
* @param {string[]} args - Command arguments
|
|
335
|
+
* @param {Object} config - CLI configuration with paths
|
|
336
|
+
*/
|
|
337
|
+
function lintCommand(args, config) {
|
|
338
|
+
const options = parseArgs(args);
|
|
339
|
+
|
|
340
|
+
// Try cache directory first, fall back to local skills
|
|
341
|
+
let skillsDir = config.skillsDir;
|
|
342
|
+
const localSkills = path.join(config.packageDir, '.agents', 'prompts', 'skills');
|
|
343
|
+
|
|
344
|
+
if (!fs.existsSync(skillsDir)) {
|
|
345
|
+
if (fs.existsSync(localSkills)) {
|
|
346
|
+
skillsDir = localSkills;
|
|
347
|
+
} else {
|
|
348
|
+
tui.error('Skills directory not found. Run "bi-superpowers unlock" first.');
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
tui.header('BI Agent Superpowers', 'Skill Linter');
|
|
354
|
+
|
|
355
|
+
// Determine which files to lint
|
|
356
|
+
let filesToLint = [];
|
|
357
|
+
|
|
358
|
+
if (options.files.length > 0) {
|
|
359
|
+
// Lint specific files
|
|
360
|
+
filesToLint = options.files
|
|
361
|
+
.map((f) => {
|
|
362
|
+
// Support both full path and just filename
|
|
363
|
+
if (fs.existsSync(f)) {
|
|
364
|
+
return f;
|
|
365
|
+
}
|
|
366
|
+
const fullPath = path.join(skillsDir, f);
|
|
367
|
+
if (fs.existsSync(fullPath)) {
|
|
368
|
+
return fullPath;
|
|
369
|
+
}
|
|
370
|
+
// Try adding .md extension
|
|
371
|
+
const withExt = path.join(skillsDir, f.endsWith('.md') ? f : `${f}.md`);
|
|
372
|
+
if (fs.existsSync(withExt)) {
|
|
373
|
+
return withExt;
|
|
374
|
+
}
|
|
375
|
+
tui.warning(`File not found: ${f}`);
|
|
376
|
+
return null;
|
|
377
|
+
})
|
|
378
|
+
.filter(Boolean);
|
|
379
|
+
} else {
|
|
380
|
+
// Lint all skills
|
|
381
|
+
filesToLint = fs
|
|
382
|
+
.readdirSync(skillsDir)
|
|
383
|
+
.filter((f) => f.endsWith('.md'))
|
|
384
|
+
.map((f) => path.join(skillsDir, f));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (filesToLint.length === 0) {
|
|
388
|
+
tui.warning('No skill files found to lint');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
tui.info(`Linting ${filesToLint.length} skill file(s)...`);
|
|
393
|
+
console.log('');
|
|
394
|
+
|
|
395
|
+
// Run linting
|
|
396
|
+
const results = filesToLint.map((file) => lintFile(file));
|
|
397
|
+
|
|
398
|
+
// Output results
|
|
399
|
+
if (options.json) {
|
|
400
|
+
console.log(JSON.stringify(results, null, 2));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Display individual results
|
|
405
|
+
results.forEach((result) => {
|
|
406
|
+
tui.lintResult(result);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Display summary
|
|
410
|
+
displaySummary(results);
|
|
411
|
+
|
|
412
|
+
// Exit with error code if there are errors
|
|
413
|
+
const hasErrors = results.some((r) => r.errors.length > 0);
|
|
414
|
+
if (hasErrors) {
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
module.exports = lintCommand;
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
});
|