skill-check 0.1.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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +360 -0
  3. package/bin/skill-check.js +11 -0
  4. package/dist/cli/main.d.ts +5 -0
  5. package/dist/cli/main.js +724 -0
  6. package/dist/core/agent-scan.d.ts +19 -0
  7. package/dist/core/agent-scan.js +88 -0
  8. package/dist/core/allowlist.d.ts +1 -0
  9. package/dist/core/allowlist.js +8 -0
  10. package/dist/core/analyze.d.ts +6 -0
  11. package/dist/core/analyze.js +72 -0
  12. package/dist/core/artifact.d.ts +2 -0
  13. package/dist/core/artifact.js +33 -0
  14. package/dist/core/baseline.d.ts +8 -0
  15. package/dist/core/baseline.js +17 -0
  16. package/dist/core/config.d.ts +2 -0
  17. package/dist/core/config.js +215 -0
  18. package/dist/core/defaults.d.ts +6 -0
  19. package/dist/core/defaults.js +34 -0
  20. package/dist/core/discovery.d.ts +2 -0
  21. package/dist/core/discovery.js +46 -0
  22. package/dist/core/duplicates.d.ts +2 -0
  23. package/dist/core/duplicates.js +60 -0
  24. package/dist/core/errors.d.ts +4 -0
  25. package/dist/core/errors.js +8 -0
  26. package/dist/core/fix.d.ts +11 -0
  27. package/dist/core/fix.js +172 -0
  28. package/dist/core/formatters.d.ts +4 -0
  29. package/dist/core/formatters.js +182 -0
  30. package/dist/core/frontmatter.d.ts +7 -0
  31. package/dist/core/frontmatter.js +39 -0
  32. package/dist/core/github-formatter.d.ts +2 -0
  33. package/dist/core/github-formatter.js +10 -0
  34. package/dist/core/html-report.d.ts +3 -0
  35. package/dist/core/html-report.js +320 -0
  36. package/dist/core/interactive-fix.d.ts +9 -0
  37. package/dist/core/interactive-fix.js +22 -0
  38. package/dist/core/links.d.ts +17 -0
  39. package/dist/core/links.js +94 -0
  40. package/dist/core/open-browser.d.ts +6 -0
  41. package/dist/core/open-browser.js +20 -0
  42. package/dist/core/plugins.d.ts +2 -0
  43. package/dist/core/plugins.js +41 -0
  44. package/dist/core/quality-score.d.ts +14 -0
  45. package/dist/core/quality-score.js +55 -0
  46. package/dist/core/report.d.ts +2 -0
  47. package/dist/core/report.js +26 -0
  48. package/dist/core/rule-engine.d.ts +2 -0
  49. package/dist/core/rule-engine.js +52 -0
  50. package/dist/core/sarif.d.ts +2 -0
  51. package/dist/core/sarif.js +48 -0
  52. package/dist/index.d.ts +11 -0
  53. package/dist/index.js +8 -0
  54. package/dist/rules/core/body.d.ts +2 -0
  55. package/dist/rules/core/body.js +39 -0
  56. package/dist/rules/core/description.d.ts +2 -0
  57. package/dist/rules/core/description.js +66 -0
  58. package/dist/rules/core/file.d.ts +2 -0
  59. package/dist/rules/core/file.js +26 -0
  60. package/dist/rules/core/frontmatter.d.ts +2 -0
  61. package/dist/rules/core/frontmatter.js +124 -0
  62. package/dist/rules/core/index.d.ts +2 -0
  63. package/dist/rules/core/index.js +12 -0
  64. package/dist/rules/core/links.d.ts +2 -0
  65. package/dist/rules/core/links.js +54 -0
  66. package/dist/types.d.ts +92 -0
  67. package/dist/types.js +1 -0
  68. package/package.json +82 -0
  69. package/schemas/config.schema.json +53 -0
  70. package/skills/skill-check/SKILL.md +76 -0
@@ -0,0 +1,172 @@
1
+ import fs from 'node:fs';
2
+ import YAML from 'yaml';
3
+ import { parseFrontmatter } from './frontmatter.js';
4
+ const DESCRIPTION_PADDING = ' Include clear triggers, constraints, and expected outcomes.';
5
+ const FRONTMATTER_FIXABLE_RULE_IDS = new Set([
6
+ 'frontmatter.required',
7
+ 'frontmatter.name_required',
8
+ 'frontmatter.description_required',
9
+ 'frontmatter.name_matches_directory',
10
+ 'frontmatter.name_slug_format',
11
+ 'frontmatter.field_order',
12
+ 'description.use_when_phrase',
13
+ 'description.min_recommended_length',
14
+ ]);
15
+ const FIXABLE_RULE_IDS = new Set([
16
+ ...FRONTMATTER_FIXABLE_RULE_IDS,
17
+ 'file.trailing_newline_single',
18
+ ]);
19
+ function normalizeTrailingNewline(content) {
20
+ const withoutTrailingBreaks = content.replace(/(?:\r?\n)+$/u, '');
21
+ const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
22
+ return `${withoutTrailingBreaks}${lineEnding}`;
23
+ }
24
+ function ensureUseWhenPhrase(description) {
25
+ const trimmed = description.trim();
26
+ if (!trimmed)
27
+ return trimmed;
28
+ if (/\buse\s+when\b/i.test(trimmed))
29
+ return trimmed;
30
+ const first = trimmed[0];
31
+ if (!first)
32
+ return `Use when ${trimmed}`;
33
+ return `Use when ${first.toLowerCase()}${trimmed.slice(1)}`;
34
+ }
35
+ function ensureMinDescriptionLength(description, minLength) {
36
+ let value = description.trim();
37
+ while (value.length < minLength) {
38
+ value = `${value}${DESCRIPTION_PADDING}`;
39
+ }
40
+ return value;
41
+ }
42
+ function defaultDescription(slug) {
43
+ return `Use when ${slug} should be selected for a task based on explicit user intent and clear context.`;
44
+ }
45
+ function orderFrontmatter(frontmatter) {
46
+ const ordered = {};
47
+ if (Object.hasOwn(frontmatter, 'name')) {
48
+ ordered.name = frontmatter.name;
49
+ }
50
+ if (Object.hasOwn(frontmatter, 'description')) {
51
+ ordered.description = frontmatter.description;
52
+ }
53
+ for (const [key, value] of Object.entries(frontmatter)) {
54
+ if (key === 'name' || key === 'description')
55
+ continue;
56
+ ordered[key] = value;
57
+ }
58
+ return ordered;
59
+ }
60
+ function renderFrontmatterContent(frontmatter, body, lineEnding) {
61
+ const yamlBlock = YAML.stringify(frontmatter)
62
+ .trimEnd()
63
+ .replace(/\n/g, lineEnding);
64
+ const normalizedBody = body.length > 0 && !body.startsWith('\n') && !body.startsWith('\r\n')
65
+ ? `${lineEnding}${body}`
66
+ : body;
67
+ return `---${lineEnding}${yamlBlock}${lineEnding}---${lineEnding}${normalizedBody}`;
68
+ }
69
+ export function isFixableRuleId(ruleId) {
70
+ return FIXABLE_RULE_IDS.has(ruleId);
71
+ }
72
+ function applyFixesForSkill(skill, ruleIds, minDescriptionChars) {
73
+ if (ruleIds.size === 0)
74
+ return [];
75
+ let content = fs.readFileSync(skill.filePath, 'utf8');
76
+ const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
77
+ const appliedRuleIds = new Set();
78
+ const needsFrontmatterFix = Array.from(ruleIds).some((ruleId) => FRONTMATTER_FIXABLE_RULE_IDS.has(ruleId));
79
+ if (needsFrontmatterFix) {
80
+ const parsed = parseFrontmatter(content);
81
+ const hasValidFrontmatter = Boolean(parsed.frontmatter);
82
+ const frontmatter = parsed.frontmatter ? { ...parsed.frontmatter } : {};
83
+ const body = parsed.body;
84
+ if (ruleIds.has('frontmatter.required') ||
85
+ ruleIds.has('frontmatter.name_required') ||
86
+ ruleIds.has('frontmatter.name_matches_directory') ||
87
+ ruleIds.has('frontmatter.name_slug_format') ||
88
+ (!hasValidFrontmatter && !frontmatter.name)) {
89
+ frontmatter.name = skill.slug;
90
+ }
91
+ const shouldEnsureDescription = ruleIds.has('frontmatter.required') ||
92
+ ruleIds.has('frontmatter.description_required') ||
93
+ ruleIds.has('description.use_when_phrase') ||
94
+ ruleIds.has('description.min_recommended_length') ||
95
+ !frontmatter.description;
96
+ if (shouldEnsureDescription) {
97
+ let description = typeof frontmatter.description === 'string'
98
+ ? frontmatter.description.trim()
99
+ : '';
100
+ if (!description) {
101
+ description = defaultDescription(skill.slug);
102
+ }
103
+ if (ruleIds.has('description.use_when_phrase')) {
104
+ description = ensureUseWhenPhrase(description);
105
+ }
106
+ if (ruleIds.has('description.min_recommended_length')) {
107
+ description = ensureMinDescriptionLength(description, minDescriptionChars);
108
+ }
109
+ frontmatter.description = description;
110
+ }
111
+ const orderedFrontmatter = orderFrontmatter(frontmatter);
112
+ const rewritten = renderFrontmatterContent(orderedFrontmatter, body, lineEnding);
113
+ if (rewritten !== content) {
114
+ content = rewritten;
115
+ for (const ruleId of ruleIds) {
116
+ if (FRONTMATTER_FIXABLE_RULE_IDS.has(ruleId)) {
117
+ appliedRuleIds.add(ruleId);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ if (ruleIds.has('file.trailing_newline_single')) {
123
+ const normalized = normalizeTrailingNewline(content);
124
+ if (normalized !== content) {
125
+ content = normalized;
126
+ appliedRuleIds.add('file.trailing_newline_single');
127
+ }
128
+ }
129
+ if (appliedRuleIds.size > 0) {
130
+ fs.writeFileSync(skill.filePath, content, 'utf8');
131
+ }
132
+ return Array.from(appliedRuleIds);
133
+ }
134
+ export function applyAutoFixes(result) {
135
+ const rulesByFile = new Map();
136
+ const skillByRelativePath = new Map(result.skills.map((skill) => [skill.relativePath, skill]));
137
+ let supportedDiagnostics = 0;
138
+ let unsupportedDiagnostics = 0;
139
+ for (const diagnostic of result.diagnostics) {
140
+ if (!isFixableRuleId(diagnostic.ruleId)) {
141
+ unsupportedDiagnostics += 1;
142
+ continue;
143
+ }
144
+ supportedDiagnostics += 1;
145
+ const skill = skillByRelativePath.get(diagnostic.file);
146
+ if (!skill)
147
+ continue;
148
+ const existing = rulesByFile.get(diagnostic.file) ?? new Set();
149
+ existing.add(diagnostic.ruleId);
150
+ rulesByFile.set(diagnostic.file, existing);
151
+ }
152
+ let appliedFixes = 0;
153
+ const updatedFiles = [];
154
+ for (const [relativePath, ruleIds] of rulesByFile) {
155
+ const skill = skillByRelativePath.get(relativePath);
156
+ if (!skill)
157
+ continue;
158
+ const applied = applyFixesForSkill(skill, ruleIds, result.config.limits.minDescriptionChars);
159
+ if (applied.length === 0)
160
+ continue;
161
+ appliedFixes += applied.length;
162
+ updatedFiles.push(relativePath);
163
+ }
164
+ return {
165
+ requestedDiagnostics: result.diagnostics.length,
166
+ supportedDiagnostics,
167
+ unsupportedDiagnostics,
168
+ appliedFixes,
169
+ filesUpdated: updatedFiles.length,
170
+ updatedFiles,
171
+ };
172
+ }
@@ -0,0 +1,4 @@
1
+ import type { AnalysisResult } from '../types.js';
2
+ import type { SkillScore } from './quality-score.js';
3
+ export declare function renderText(result: AnalysisResult, scores?: SkillScore[]): string;
4
+ export declare function toJson(result: AnalysisResult): Record<string, unknown>;
@@ -0,0 +1,182 @@
1
+ import boxen from 'boxen';
2
+ import Table from 'cli-table3';
3
+ import pc from 'picocolors';
4
+ function groupByFile(diagnostics) {
5
+ const grouped = new Map();
6
+ for (const diagnostic of diagnostics) {
7
+ const list = grouped.get(diagnostic.file) ?? [];
8
+ list.push(diagnostic);
9
+ grouped.set(diagnostic.file, list);
10
+ }
11
+ return grouped;
12
+ }
13
+ function renderDivider(char = '=') {
14
+ return char.repeat(72);
15
+ }
16
+ function renderSeverityLabel(severity) {
17
+ if (severity === 'error')
18
+ return pc.red(pc.bold('[ERROR]'));
19
+ return pc.yellow(pc.bold('[WARN ]'));
20
+ }
21
+ function renderStatusTag(summary) {
22
+ if (summary.errorCount > 0)
23
+ return pc.red(pc.bold('[FAIL]'));
24
+ if (summary.warningCount > 0)
25
+ return pc.yellow(pc.bold('[WARN]'));
26
+ return pc.green(pc.bold('[PASS]'));
27
+ }
28
+ function createAsciiTable() {
29
+ return new Table({
30
+ chars: {
31
+ top: '-',
32
+ 'top-mid': '+',
33
+ 'top-left': '+',
34
+ 'top-right': '+',
35
+ bottom: '-',
36
+ 'bottom-mid': '+',
37
+ 'bottom-left': '+',
38
+ 'bottom-right': '+',
39
+ left: '|',
40
+ 'left-mid': '+',
41
+ mid: '-',
42
+ 'mid-mid': '+',
43
+ right: '|',
44
+ 'right-mid': '+',
45
+ middle: '|',
46
+ },
47
+ style: {
48
+ border: [],
49
+ compact: true,
50
+ head: [],
51
+ },
52
+ wordWrap: true,
53
+ });
54
+ }
55
+ function renderOverviewTable(result, fileCount) {
56
+ const status = result.summary.errorCount > 0
57
+ ? pc.red('FAIL')
58
+ : result.summary.warningCount > 0
59
+ ? pc.yellow('WARN')
60
+ : pc.green('PASS');
61
+ const table = createAsciiTable();
62
+ table.push([
63
+ pc.bold('Skills scanned'),
64
+ pc.cyan(String(result.summary.skillCount)),
65
+ pc.bold('Files flagged'),
66
+ pc.cyan(String(fileCount)),
67
+ ], [
68
+ pc.bold('Diagnostics'),
69
+ pc.cyan(String(result.diagnostics.length)),
70
+ pc.bold('Errors'),
71
+ pc.red(String(result.summary.errorCount)),
72
+ ], [
73
+ pc.bold('Warnings'),
74
+ pc.yellow(String(result.summary.warningCount)),
75
+ pc.bold('Status'),
76
+ pc.bold(status),
77
+ ]);
78
+ return table.toString();
79
+ }
80
+ function renderSummaryPanel(result) {
81
+ const status = result.summary.errorCount > 0
82
+ ? pc.red(pc.bold('FAIL'))
83
+ : result.summary.warningCount > 0
84
+ ? pc.yellow(pc.bold('WARN'))
85
+ : pc.green(pc.bold('PASS'));
86
+ return boxen([
87
+ `${pc.bold('Status')} ${status}`,
88
+ `${pc.bold('Skills')} ${pc.cyan(String(result.summary.skillCount))}`,
89
+ `${pc.bold('Errors')} ${pc.red(String(result.summary.errorCount))}`,
90
+ `${pc.bold('Warnings')} ${pc.yellow(String(result.summary.warningCount))}`,
91
+ ].join('\n'), {
92
+ borderColor: result.summary.errorCount > 0
93
+ ? 'red'
94
+ : result.summary.warningCount > 0
95
+ ? 'yellow'
96
+ : 'green',
97
+ borderStyle: 'classic',
98
+ padding: {
99
+ top: 0,
100
+ right: 1,
101
+ bottom: 0,
102
+ left: 1,
103
+ },
104
+ });
105
+ }
106
+ function renderScoreBar(score) {
107
+ const color = score >= 80 ? pc.green : score >= 50 ? pc.yellow : pc.red;
108
+ const filled = Math.round(score / 5);
109
+ const empty = 20 - filled;
110
+ return `${color('█'.repeat(filled))}${pc.dim('░'.repeat(empty))} ${color(String(score))}`;
111
+ }
112
+ export function renderText(result, scores) {
113
+ const lines = [];
114
+ const grouped = groupByFile(result.diagnostics);
115
+ const status = renderStatusTag(result.summary);
116
+ lines.push(pc.dim(renderDivider('=')));
117
+ lines.push(`${pc.bold('SKILL-CHECK VALIDATION REPORT')} ${status}`);
118
+ lines.push(pc.dim(renderDivider('=')));
119
+ lines.push(pc.bold('Overview'));
120
+ lines.push(pc.dim(renderDivider('-')));
121
+ lines.push(renderOverviewTable(result, grouped.size));
122
+ lines.push('');
123
+ if (grouped.size === 0) {
124
+ lines.push(pc.bold('Results'));
125
+ lines.push(pc.dim(renderDivider('-')));
126
+ lines.push(`${pc.green('[OK]')} No diagnostics found.`);
127
+ lines.push('');
128
+ }
129
+ else {
130
+ lines.push(pc.bold('Results'));
131
+ lines.push(pc.dim(renderDivider('-')));
132
+ const sortedFiles = Array.from(grouped.keys()).sort();
133
+ for (const file of sortedFiles) {
134
+ const diagnostics = grouped.get(file);
135
+ if (!diagnostics)
136
+ continue;
137
+ lines.push(`${pc.bold('[FILE]')} ${pc.bold(file)}`);
138
+ lines.push(` ${pc.dim(`diagnostics: ${diagnostics.length}`)}`);
139
+ lines.push('');
140
+ for (const diagnostic of diagnostics) {
141
+ const location = `${diagnostic.line}:${diagnostic.column}`;
142
+ lines.push(` ${renderSeverityLabel(diagnostic.severity)} ${pc.cyan(diagnostic.ruleId)} ${pc.dim(`at ${location}`)}`);
143
+ lines.push(` message : ${diagnostic.message}`);
144
+ if (diagnostic.suggestion) {
145
+ lines.push(` suggestion : ${pc.yellow(diagnostic.suggestion)}`);
146
+ }
147
+ lines.push('');
148
+ }
149
+ }
150
+ }
151
+ if (scores && scores.length > 0) {
152
+ lines.push(pc.bold('Quality Scores'));
153
+ lines.push(pc.dim(renderDivider('-')));
154
+ for (const s of scores) {
155
+ lines.push(` ${renderScoreBar(s.score)} ${pc.dim(s.relativePath)}`);
156
+ }
157
+ lines.push('');
158
+ }
159
+ lines.push(pc.dim(renderDivider('-')));
160
+ lines.push(renderSummaryPanel(result));
161
+ lines.push('');
162
+ const statusWord = result.summary.errorCount > 0
163
+ ? pc.red('FAIL')
164
+ : result.summary.warningCount > 0
165
+ ? pc.yellow('WARN')
166
+ : pc.green('PASS');
167
+ lines.push(`Summary: skills=${pc.cyan(String(result.summary.skillCount))} errors=${pc.red(String(result.summary.errorCount))} warnings=${pc.yellow(String(result.summary.warningCount))} status=${statusWord}`);
168
+ lines.push(pc.dim(renderDivider('-')));
169
+ return `${lines.join('\n').trimEnd()}\n`;
170
+ }
171
+ export function toJson(result) {
172
+ return {
173
+ summary: result.summary,
174
+ diagnostics: result.diagnostics,
175
+ metadata: {
176
+ roots: result.config.roots,
177
+ include: result.config.include,
178
+ exclude: result.config.exclude,
179
+ format: result.config.output.format,
180
+ },
181
+ };
182
+ }
@@ -0,0 +1,7 @@
1
+ export interface FrontmatterParseResult {
2
+ frontmatter: Record<string, unknown> | null;
3
+ frontmatterRaw: string | null;
4
+ body: string;
5
+ error?: string;
6
+ }
7
+ export declare function parseFrontmatter(content: string): FrontmatterParseResult;
@@ -0,0 +1,39 @@
1
+ import YAML from 'yaml';
2
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
3
+ export function parseFrontmatter(content) {
4
+ const match = content.match(FRONTMATTER_RE);
5
+ if (!match) {
6
+ return {
7
+ frontmatter: null,
8
+ frontmatterRaw: null,
9
+ body: content,
10
+ };
11
+ }
12
+ const raw = match[1];
13
+ const body = content.slice(match[0].length);
14
+ try {
15
+ const parsed = YAML.parse(raw);
16
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
17
+ return {
18
+ frontmatter: null,
19
+ frontmatterRaw: raw,
20
+ body,
21
+ error: 'frontmatter must be a YAML object',
22
+ };
23
+ }
24
+ return {
25
+ frontmatter: parsed,
26
+ frontmatterRaw: raw,
27
+ body,
28
+ };
29
+ }
30
+ catch (error) {
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ return {
33
+ frontmatter: null,
34
+ frontmatterRaw: raw,
35
+ body,
36
+ error: `invalid YAML frontmatter: ${message}`,
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,2 @@
1
+ import type { AnalysisResult } from '../types.js';
2
+ export declare function toGitHubAnnotations(result: AnalysisResult): string;
@@ -0,0 +1,10 @@
1
+ export function toGitHubAnnotations(result) {
2
+ const lines = [];
3
+ for (const d of result.diagnostics) {
4
+ const level = d.severity === 'error' ? 'error' : 'warning';
5
+ const title = d.ruleId;
6
+ const msg = d.suggestion ? `${d.message} (${d.suggestion})` : d.message;
7
+ lines.push(`::${level} file=${d.file},line=${d.line},col=${d.column},title=${title}::${msg}`);
8
+ }
9
+ return lines.length > 0 ? `${lines.join('\n')}\n` : '';
10
+ }
@@ -0,0 +1,3 @@
1
+ import type { AnalysisResult } from '../types.js';
2
+ import type { SkillScore } from './quality-score.js';
3
+ export declare function renderHtml(result: AnalysisResult, scores?: SkillScore[]): string;