mirror of
https://github.com/supabase/agent-skills.git
synced 2026-01-26 19:09:51 +08:00
Add Biome formatter/linter and restore CI workflow (#6)
- Install Biome as the project formatter and linter
- Configure Biome with recommended settings
- Add format, lint, and check scripts to package.json
- Restore CI workflow from git history (commit 0a543e1)
- Extend CI with new Biome job for format and lint checks
- Apply Biome formatting to all TypeScript files
- Fix linting issues (use node: protocol, template literals, forEach pattern)
CI now runs on:
- All pushes to main branch
- All pull requests
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,261 +1,275 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { basename } from "path";
|
||||
import type { Rule, CodeExample, ImpactLevel, ParseResult } from "./types.js";
|
||||
import { SECTION_MAP, IMPACT_LEVELS } from "./config.js";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { basename } from "node:path";
|
||||
import { IMPACT_LEVELS, SECTION_MAP } from "./config.js";
|
||||
import type { CodeExample, ImpactLevel, ParseResult, Rule } from "./types.js";
|
||||
|
||||
/**
|
||||
* Parse YAML-style frontmatter from markdown content
|
||||
*/
|
||||
function parseFrontmatter(content: string): {
|
||||
frontmatter: Record<string, string>;
|
||||
body: string;
|
||||
frontmatter: Record<string, string>;
|
||||
body: string;
|
||||
} {
|
||||
const frontmatter: Record<string, string> = {};
|
||||
const frontmatter: Record<string, string> = {};
|
||||
|
||||
if (!content.startsWith("---")) {
|
||||
return { frontmatter, body: content };
|
||||
}
|
||||
if (!content.startsWith("---")) {
|
||||
return { frontmatter, body: content };
|
||||
}
|
||||
|
||||
const endIndex = content.indexOf("---", 3);
|
||||
if (endIndex === -1) {
|
||||
return { frontmatter, body: content };
|
||||
}
|
||||
const endIndex = content.indexOf("---", 3);
|
||||
if (endIndex === -1) {
|
||||
return { frontmatter, body: content };
|
||||
}
|
||||
|
||||
const frontmatterContent = content.slice(3, endIndex).trim();
|
||||
const body = content.slice(endIndex + 3).trim();
|
||||
const frontmatterContent = content.slice(3, endIndex).trim();
|
||||
const body = content.slice(endIndex + 3).trim();
|
||||
|
||||
for (const line of frontmatterContent.split("\n")) {
|
||||
const colonIndex = line.indexOf(":");
|
||||
if (colonIndex === -1) continue;
|
||||
for (const line of frontmatterContent.split("\n")) {
|
||||
const colonIndex = line.indexOf(":");
|
||||
if (colonIndex === -1) continue;
|
||||
|
||||
const key = line.slice(0, colonIndex).trim();
|
||||
let value = line.slice(colonIndex + 1).trim();
|
||||
const key = line.slice(0, colonIndex).trim();
|
||||
let value = line.slice(colonIndex + 1).trim();
|
||||
|
||||
// Strip quotes
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
// Strip quotes
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
frontmatter[key] = value;
|
||||
}
|
||||
frontmatter[key] = value;
|
||||
}
|
||||
|
||||
return { frontmatter, body };
|
||||
return { frontmatter, body };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract section number from filename prefix
|
||||
*/
|
||||
function getSectionFromFilename(filename: string): number | null {
|
||||
const base = basename(filename, ".md");
|
||||
const prefix = base.split("-")[0];
|
||||
return SECTION_MAP[prefix] ?? null;
|
||||
const base = basename(filename, ".md");
|
||||
const prefix = base.split("-")[0];
|
||||
return SECTION_MAP[prefix] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract code examples from markdown body
|
||||
*/
|
||||
function extractExamples(body: string): CodeExample[] {
|
||||
const examples: CodeExample[] = [];
|
||||
const lines = body.split("\n");
|
||||
const examples: CodeExample[] = [];
|
||||
const lines = body.split("\n");
|
||||
|
||||
let currentLabel = "";
|
||||
let currentDescription = "";
|
||||
let inCodeBlock = false;
|
||||
let codeBlockLang = "";
|
||||
let codeBlockContent: string[] = [];
|
||||
let additionalText: string[] = [];
|
||||
let currentLabel = "";
|
||||
let currentDescription = "";
|
||||
let inCodeBlock = false;
|
||||
let codeBlockLang = "";
|
||||
let codeBlockContent: string[] = [];
|
||||
let additionalText: string[] = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Check for example label: **Label:** or **Label (description):**
|
||||
const labelMatch = line.match(/^\*\*([^*]+?)(?:\s*\(([^)]+)\))?\s*:\*\*\s*$/);
|
||||
if (labelMatch && !inCodeBlock) {
|
||||
// Save previous example if exists
|
||||
if (currentLabel && codeBlockContent.length > 0) {
|
||||
examples.push({
|
||||
label: currentLabel,
|
||||
description: currentDescription || undefined,
|
||||
code: codeBlockContent.join("\n"),
|
||||
language: codeBlockLang || undefined,
|
||||
additionalText: additionalText.length > 0 ? additionalText.join("\n").trim() : undefined,
|
||||
});
|
||||
}
|
||||
// Check for example label: **Label:** or **Label (description):**
|
||||
const labelMatch = line.match(
|
||||
/^\*\*([^*]+?)(?:\s*\(([^)]+)\))?\s*:\*\*\s*$/,
|
||||
);
|
||||
if (labelMatch && !inCodeBlock) {
|
||||
// Save previous example if exists
|
||||
if (currentLabel && codeBlockContent.length > 0) {
|
||||
examples.push({
|
||||
label: currentLabel,
|
||||
description: currentDescription || undefined,
|
||||
code: codeBlockContent.join("\n"),
|
||||
language: codeBlockLang || undefined,
|
||||
additionalText:
|
||||
additionalText.length > 0
|
||||
? additionalText.join("\n").trim()
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
currentLabel = labelMatch[1].trim();
|
||||
currentDescription = labelMatch[2]?.trim() || "";
|
||||
codeBlockContent = [];
|
||||
codeBlockLang = "";
|
||||
additionalText = [];
|
||||
continue;
|
||||
}
|
||||
currentLabel = labelMatch[1].trim();
|
||||
currentDescription = labelMatch[2]?.trim() || "";
|
||||
codeBlockContent = [];
|
||||
codeBlockLang = "";
|
||||
additionalText = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for code block start
|
||||
if (line.startsWith("```") && !inCodeBlock) {
|
||||
inCodeBlock = true;
|
||||
codeBlockLang = line.slice(3).trim();
|
||||
continue;
|
||||
}
|
||||
// Check for code block start
|
||||
if (line.startsWith("```") && !inCodeBlock) {
|
||||
inCodeBlock = true;
|
||||
codeBlockLang = line.slice(3).trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for code block end
|
||||
if (line.startsWith("```") && inCodeBlock) {
|
||||
inCodeBlock = false;
|
||||
continue;
|
||||
}
|
||||
// Check for code block end
|
||||
if (line.startsWith("```") && inCodeBlock) {
|
||||
inCodeBlock = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect code block content
|
||||
if (inCodeBlock) {
|
||||
codeBlockContent.push(line);
|
||||
continue;
|
||||
}
|
||||
// Collect code block content
|
||||
if (inCodeBlock) {
|
||||
codeBlockContent.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect additional text after code block (before next label)
|
||||
if (currentLabel && codeBlockContent.length > 0 && line.trim()) {
|
||||
// Stop collecting if we hit a heading or reference
|
||||
if (line.startsWith("#") || line.startsWith("Reference")) {
|
||||
continue;
|
||||
}
|
||||
additionalText.push(line);
|
||||
}
|
||||
}
|
||||
// Collect additional text after code block (before next label)
|
||||
if (currentLabel && codeBlockContent.length > 0 && line.trim()) {
|
||||
// Stop collecting if we hit a heading or reference
|
||||
if (line.startsWith("#") || line.startsWith("Reference")) {
|
||||
continue;
|
||||
}
|
||||
additionalText.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Save last example
|
||||
if (currentLabel && codeBlockContent.length > 0) {
|
||||
examples.push({
|
||||
label: currentLabel,
|
||||
description: currentDescription || undefined,
|
||||
code: codeBlockContent.join("\n"),
|
||||
language: codeBlockLang || undefined,
|
||||
additionalText: additionalText.length > 0 ? additionalText.join("\n").trim() : undefined,
|
||||
});
|
||||
}
|
||||
// Save last example
|
||||
if (currentLabel && codeBlockContent.length > 0) {
|
||||
examples.push({
|
||||
label: currentLabel,
|
||||
description: currentDescription || undefined,
|
||||
code: codeBlockContent.join("\n"),
|
||||
language: codeBlockLang || undefined,
|
||||
additionalText:
|
||||
additionalText.length > 0
|
||||
? additionalText.join("\n").trim()
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return examples;
|
||||
return examples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract title from first ## heading
|
||||
*/
|
||||
function extractTitle(body: string): string | null {
|
||||
const match = body.match(/^##\s+(.+)$/m);
|
||||
return match ? match[1].trim() : null;
|
||||
const match = body.match(/^##\s+(.+)$/m);
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract explanation (content between title and first example)
|
||||
*/
|
||||
function extractExplanation(body: string): string {
|
||||
const lines = body.split("\n");
|
||||
const explanationLines: string[] = [];
|
||||
let foundTitle = false;
|
||||
const lines = body.split("\n");
|
||||
const explanationLines: string[] = [];
|
||||
let foundTitle = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("## ")) {
|
||||
foundTitle = true;
|
||||
continue;
|
||||
}
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("## ")) {
|
||||
foundTitle = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!foundTitle) continue;
|
||||
if (!foundTitle) continue;
|
||||
|
||||
// Stop at first example label or code block
|
||||
if (line.match(/^\*\*[^*]+:\*\*/) || line.startsWith("```")) {
|
||||
break;
|
||||
}
|
||||
// Stop at first example label or code block
|
||||
if (line.match(/^\*\*[^*]+:\*\*/) || line.startsWith("```")) {
|
||||
break;
|
||||
}
|
||||
|
||||
explanationLines.push(line);
|
||||
}
|
||||
explanationLines.push(line);
|
||||
}
|
||||
|
||||
return explanationLines.join("\n").trim();
|
||||
return explanationLines.join("\n").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract references from body
|
||||
*/
|
||||
function extractReferences(body: string): string[] {
|
||||
const references: string[] = [];
|
||||
const lines = body.split("\n");
|
||||
const references: string[] = [];
|
||||
const lines = body.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
// Match "Reference: [text](url)" or "- [text](url)" after "References:"
|
||||
const refMatch = line.match(/Reference:\s*\[([^\]]+)\]\(([^)]+)\)/);
|
||||
if (refMatch) {
|
||||
references.push(refMatch[2]);
|
||||
continue;
|
||||
}
|
||||
for (const line of lines) {
|
||||
// Match "Reference: [text](url)" or "- [text](url)" after "References:"
|
||||
const refMatch = line.match(/Reference:\s*\[([^\]]+)\]\(([^)]+)\)/);
|
||||
if (refMatch) {
|
||||
references.push(refMatch[2]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match list items under References section
|
||||
const listMatch = line.match(/^-\s*\[([^\]]+)\]\(([^)]+)\)/);
|
||||
if (listMatch) {
|
||||
references.push(listMatch[2]);
|
||||
}
|
||||
}
|
||||
// Match list items under References section
|
||||
const listMatch = line.match(/^-\s*\[([^\]]+)\]\(([^)]+)\)/);
|
||||
if (listMatch) {
|
||||
references.push(listMatch[2]);
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
return references;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a rule file and return structured data
|
||||
*/
|
||||
export function parseRuleFile(filePath: string): ParseResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const { frontmatter, body } = parseFrontmatter(content);
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const { frontmatter, body } = parseFrontmatter(content);
|
||||
|
||||
// Extract section from filename
|
||||
const section = getSectionFromFilename(filePath);
|
||||
if (section === null) {
|
||||
errors.push(`Could not determine section from filename: ${basename(filePath)}`);
|
||||
return { success: false, errors, warnings };
|
||||
}
|
||||
// Extract section from filename
|
||||
const section = getSectionFromFilename(filePath);
|
||||
if (section === null) {
|
||||
errors.push(
|
||||
`Could not determine section from filename: ${basename(filePath)}`,
|
||||
);
|
||||
return { success: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Get title from frontmatter or body
|
||||
const title = frontmatter.title || extractTitle(body);
|
||||
if (!title) {
|
||||
errors.push("Missing title in frontmatter or body");
|
||||
return { success: false, errors, warnings };
|
||||
}
|
||||
// Get title from frontmatter or body
|
||||
const title = frontmatter.title || extractTitle(body);
|
||||
if (!title) {
|
||||
errors.push("Missing title in frontmatter or body");
|
||||
return { success: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Get impact level
|
||||
const impact = frontmatter.impact as ImpactLevel;
|
||||
if (!impact || !IMPACT_LEVELS.includes(impact)) {
|
||||
errors.push(`Invalid or missing impact level: ${impact}. Must be one of: ${IMPACT_LEVELS.join(", ")}`);
|
||||
return { success: false, errors, warnings };
|
||||
}
|
||||
// Get impact level
|
||||
const impact = frontmatter.impact as ImpactLevel;
|
||||
if (!impact || !IMPACT_LEVELS.includes(impact)) {
|
||||
errors.push(
|
||||
`Invalid or missing impact level: ${impact}. Must be one of: ${IMPACT_LEVELS.join(", ")}`,
|
||||
);
|
||||
return { success: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Extract other fields
|
||||
const explanation = extractExplanation(body);
|
||||
const examples = extractExamples(body);
|
||||
// Extract other fields
|
||||
const explanation = extractExplanation(body);
|
||||
const examples = extractExamples(body);
|
||||
|
||||
const tags = frontmatter.tags?.split(",").map((t) => t.trim()) || [];
|
||||
const tags = frontmatter.tags?.split(",").map((t) => t.trim()) || [];
|
||||
|
||||
// Validation warnings
|
||||
if (!explanation || explanation.length < 20) {
|
||||
warnings.push("Explanation is very short or missing");
|
||||
}
|
||||
// Validation warnings
|
||||
if (!explanation || explanation.length < 20) {
|
||||
warnings.push("Explanation is very short or missing");
|
||||
}
|
||||
|
||||
if (examples.length === 0) {
|
||||
warnings.push("No code examples found");
|
||||
}
|
||||
if (examples.length === 0) {
|
||||
warnings.push("No code examples found");
|
||||
}
|
||||
|
||||
const rule: Rule = {
|
||||
id: "", // Will be assigned during build
|
||||
title,
|
||||
section,
|
||||
impact,
|
||||
impactDescription: frontmatter.impactDescription,
|
||||
explanation,
|
||||
examples,
|
||||
references: extractReferences(body),
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
};
|
||||
const rule: Rule = {
|
||||
id: "", // Will be assigned during build
|
||||
title,
|
||||
section,
|
||||
impact,
|
||||
impactDescription: frontmatter.impactDescription,
|
||||
explanation,
|
||||
examples,
|
||||
references: extractReferences(body),
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
};
|
||||
|
||||
return { success: true, rule, errors, warnings };
|
||||
} catch (error) {
|
||||
errors.push(`Failed to parse file: ${error}`);
|
||||
return { success: false, errors, warnings };
|
||||
}
|
||||
return { success: true, rule, errors, warnings };
|
||||
} catch (error) {
|
||||
errors.push(`Failed to parse file: ${error}`);
|
||||
return { success: false, errors, warnings };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user