@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,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
+ });