Files
pbakaus_impeccable/scripts/lib/utils.js
T
Paul BakausandClaude Opus 4.5 97c83fc51a Add animated mesh gradient hero with overlay content
- Add @paper-design/shaders for WebGL mesh gradient effect
- Create hero-shader.js with liquid ink gradient (grayscale palette)
- Add overlay content: title, tagline, and CTA on shader
- Update hero section structure and styling
- Fix accessibility: gallery dot buttons, toggle ARIA attributes
- Fix undefined --color-terracotta CSS variable
- Update distribution structure for Codex/Cursor skills

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 17:46:49 -08:00

333 lines
9.3 KiB
JavaScript

import fs from 'fs';
import path from 'path';
/**
* Parse frontmatter from markdown content
* Returns { frontmatter: object, body: string }
*/
export function parseFrontmatter(content) {
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
const match = content.match(frontmatterRegex);
if (!match) {
return { frontmatter: {}, body: content };
}
const [, frontmatterText, body] = match;
const frontmatter = {};
// Simple YAML parser (handles basic key-value and arrays)
const lines = frontmatterText.split('\n');
let currentKey = null;
let currentArray = null;
for (const line of lines) {
if (!line.trim()) continue;
// Calculate indent level
const leadingSpaces = line.length - line.trimStart().length;
const trimmed = line.trim();
// Array item at level 2 (nested under a key)
if (trimmed.startsWith('- ') && leadingSpaces >= 2) {
if (currentArray) {
if (trimmed.startsWith('- name:')) {
// New object in array
const obj = {};
obj.name = trimmed.slice(7).trim();
currentArray.push(obj);
}
}
continue;
}
// Property of array object (indented further)
if (leadingSpaces >= 4 && currentArray && currentArray.length > 0) {
const colonIndex = trimmed.indexOf(':');
if (colonIndex > 0) {
const key = trimmed.slice(0, colonIndex).trim();
const value = trimmed.slice(colonIndex + 1).trim();
const lastObj = currentArray[currentArray.length - 1];
lastObj[key] = value === 'true' ? true : value === 'false' ? false : value;
}
continue;
}
// Top-level key-value pair
if (leadingSpaces === 0) {
const colonIndex = trimmed.indexOf(':');
if (colonIndex > 0) {
const key = trimmed.slice(0, colonIndex).trim();
const value = trimmed.slice(colonIndex + 1).trim();
if (value) {
frontmatter[key] = value;
currentKey = key;
currentArray = null;
} else {
// Start of array
currentKey = key;
currentArray = [];
frontmatter[key] = currentArray;
}
}
}
}
return { frontmatter, body: body.trim() };
}
/**
* Recursively read all .md files from a directory
*/
export function readFilesRecursive(dir, fileList = []) {
if (!fs.existsSync(dir)) {
return fileList;
}
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
readFilesRecursive(filePath, fileList);
} else if (file.endsWith('.md')) {
fileList.push(filePath);
}
}
return fileList;
}
/**
* Read and parse all source files
* Supports both:
* - Single file skills: source/skills/{name}.md
* - Directory skills: source/skills/{name}/SKILL.md + reference/*.md
*/
export function readSourceFiles(rootDir) {
const commandsDir = path.join(rootDir, 'source/commands');
const skillsDir = path.join(rootDir, 'source/skills');
const commandFiles = readFilesRecursive(commandsDir);
const commands = commandFiles.map(filePath => {
const content = fs.readFileSync(filePath, 'utf-8');
const { frontmatter, body } = parseFrontmatter(content);
const name = path.basename(filePath, '.md');
return {
name: frontmatter.name || name,
description: frontmatter.description || '',
args: frontmatter.args || [],
body,
filePath
};
});
// Read skills - handling both file and directory formats
const skills = [];
if (fs.existsSync(skillsDir)) {
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(skillsDir, entry.name);
if (entry.isDirectory()) {
// Directory-based skill with potential references
const skillMdPath = path.join(entryPath, 'SKILL.md');
if (fs.existsSync(skillMdPath)) {
const content = fs.readFileSync(skillMdPath, 'utf-8');
const { frontmatter, body } = parseFrontmatter(content);
// Read reference files if they exist
const references = [];
const referenceDir = path.join(entryPath, 'reference');
if (fs.existsSync(referenceDir)) {
const refFiles = fs.readdirSync(referenceDir).filter(f => f.endsWith('.md'));
for (const refFile of refFiles) {
const refPath = path.join(referenceDir, refFile);
const refContent = fs.readFileSync(refPath, 'utf-8');
references.push({
name: path.basename(refFile, '.md'),
content: refContent,
filePath: refPath
});
}
}
skills.push({
name: frontmatter.name || entry.name,
description: frontmatter.description || '',
license: frontmatter.license || '',
compatibility: frontmatter.compatibility || '',
metadata: frontmatter.metadata || null,
allowedTools: frontmatter['allowed-tools'] || '',
body,
filePath: skillMdPath,
references
});
}
} else if (entry.name.endsWith('.md')) {
// Single file skill (legacy format)
const content = fs.readFileSync(entryPath, 'utf-8');
const { frontmatter, body } = parseFrontmatter(content);
const name = path.basename(entry.name, '.md');
skills.push({
name: frontmatter.name || name,
description: frontmatter.description || '',
license: frontmatter.license || '',
compatibility: frontmatter.compatibility || '',
metadata: frontmatter.metadata || null,
allowedTools: frontmatter['allowed-tools'] || '',
body,
filePath: entryPath,
references: []
});
}
}
}
return { commands, skills };
}
/**
* Ensure directory exists, create if needed
*/
export function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
/**
* Clean directory (remove all contents)
*/
export function cleanDir(dirPath) {
if (fs.existsSync(dirPath)) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
}
/**
* Write file with automatic directory creation
*/
export function writeFile(filePath, content) {
const dir = path.dirname(filePath);
ensureDir(dir);
fs.writeFileSync(filePath, content, 'utf-8');
}
/**
* Read and parse patterns.md
* Returns { patterns: [...], antipatterns: [...], body: string }
*/
export function readPatterns(rootDir) {
const filePath = path.join(rootDir, 'source/patterns.md');
if (!fs.existsSync(filePath)) {
return { patterns: [], antipatterns: [], body: '' };
}
const content = fs.readFileSync(filePath, 'utf-8');
// Split frontmatter and body
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
const match = content.match(frontmatterRegex);
if (!match) {
return { patterns: [], antipatterns: [], body: content };
}
const [, frontmatterText, body] = match;
// Parse both patterns and antipatterns sections
const patterns = [];
const antipatterns = [];
const lines = frontmatterText.split('\n');
let currentSection = null; // 'patterns' or 'antipatterns'
let currentCategory = null;
let inItems = false;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
const indent = line.length - line.trimStart().length;
// Top-level section declaration
if (indent === 0 && trimmed === 'patterns:') {
currentSection = 'patterns';
currentCategory = null;
inItems = false;
continue;
}
if (indent === 0 && trimmed === 'antipatterns:') {
currentSection = 'antipatterns';
currentCategory = null;
inItems = false;
continue;
}
// New category starts with "- name:"
if (trimmed.startsWith('- name:') && currentSection) {
currentCategory = {
name: trimmed.slice(7).trim(),
items: []
};
if (currentSection === 'patterns') {
patterns.push(currentCategory);
} else {
antipatterns.push(currentCategory);
}
inItems = false;
continue;
}
// Items array declaration
if (trimmed === 'items:' && currentCategory) {
inItems = true;
continue;
}
// Item within items array (indented with "- ")
if (trimmed.startsWith('- ') && inItems && currentCategory && indent >= 6) {
currentCategory.items.push(trimmed.slice(2).trim());
}
}
return { patterns, antipatterns, body: body.trim() };
}
/**
* Generate YAML frontmatter string
*/
export function generateYamlFrontmatter(data) {
const lines = ['---'];
for (const [key, value] of Object.entries(data)) {
if (Array.isArray(value)) {
lines.push(`${key}:`);
for (const item of value) {
if (typeof item === 'object') {
lines.push(` - name: ${item.name}`);
if (item.description) lines.push(` description: ${item.description}`);
if (item.required !== undefined) lines.push(` required: ${item.required}`);
} else {
lines.push(` - ${item}`);
}
}
} else {
lines.push(`${key}: ${value}`);
}
}
lines.push('---');
return lines.join('\n');
}