mirror of
https://github.com/Jeffallan/claude-skills.git
synced 2026-09-14 18:43:38 +08:00
3385184ef5
The bulk SKILL.md update appends a [Documentation](docs-site-url) line so that aggregators (skills.sh, etc.) consuming raw source markdown render a real <a href> backlink to the docs site for SEO. On the docs site itself that URL points at the page rendering it — a self-link with no value and visual redundancy. syncSkillPages now strips that exact line at build time so the docs site, public markdown mirrors, llms.txt, and llms-full.txt all render clean while the source files retain the line for downstream syndication. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
744 lines
24 KiB
JavaScript
744 lines
24 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* sync-content.mjs
|
|
*
|
|
* Pre-build script that transforms repo-root content into Starlight-compatible
|
|
* pages under site/src/content/docs/. Run via `npm run sync`.
|
|
*/
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import yaml from 'js-yaml';
|
|
|
|
const ROOT = path.resolve(import.meta.dirname, '..', '..');
|
|
const DOCS_DIR = path.resolve(import.meta.dirname, '..', 'src', 'content', 'docs');
|
|
const PUBLIC_DIR = path.resolve(import.meta.dirname, '..', 'public');
|
|
|
|
const GITHUB_BLOB = 'https://github.com/jeffallan/claude-skills/blob/main';
|
|
const BASE_PATH = '/claude-skills';
|
|
|
|
const pageManifest = []; // { siteUrl, title, description, category, contentFile }
|
|
|
|
// ─── Domain label mapping ───────────────────────────────────────────
|
|
const DOMAIN_LABELS = {
|
|
language: 'Language',
|
|
backend: 'Backend Frameworks',
|
|
frontend: 'Frontend & Mobile',
|
|
infrastructure: 'Infrastructure & Cloud',
|
|
'api-architecture': 'API & Architecture',
|
|
quality: 'Quality & Testing',
|
|
devops: 'DevOps & Operations',
|
|
security: 'Security',
|
|
'data-ml': 'Data & ML',
|
|
platform: 'Platform',
|
|
specialized: 'Specialized',
|
|
workflow: 'Workflow Skills',
|
|
};
|
|
|
|
// ─── Core docs mapping (source relative to ROOT → dest relative to DOCS_DIR)
|
|
const CORE_DOCS = [
|
|
{
|
|
src: 'QUICKSTART.md',
|
|
dest: 'getting-started.md',
|
|
title: 'Getting Started',
|
|
description: 'Installation and first steps',
|
|
category: 'docs',
|
|
},
|
|
{
|
|
src: 'SKILLS_GUIDE.md',
|
|
dest: 'skills-guide.md',
|
|
title: 'Skills Guide',
|
|
description: 'Decision trees and skill combinations for choosing the right skill',
|
|
category: 'docs',
|
|
},
|
|
{
|
|
src: 'README.md',
|
|
dest: 'readme.md',
|
|
title: 'README',
|
|
description: 'Project overview, architecture, and usage patterns',
|
|
category: 'project',
|
|
},
|
|
{
|
|
src: 'CONTRIBUTING.md',
|
|
dest: 'contributing.md',
|
|
title: 'Contributing',
|
|
description: 'How to contribute new skills and improve existing ones',
|
|
category: 'project',
|
|
},
|
|
{
|
|
src: 'CHANGELOG.md',
|
|
dest: 'changelog.md',
|
|
title: 'Changelog',
|
|
description: 'Version history and release notes',
|
|
category: 'project',
|
|
},
|
|
{
|
|
src: 'ROADMAP.md',
|
|
dest: 'roadmap.md',
|
|
title: 'Roadmap',
|
|
description: 'Planned features and future direction',
|
|
category: 'project',
|
|
},
|
|
];
|
|
|
|
// ─── Guide docs mapping ─────────────────────────────────────────────
|
|
const GUIDE_DOCS = [
|
|
{
|
|
src: 'docs/WORKFLOW_COMMANDS.md',
|
|
dest: 'guides/workflow-commands.md',
|
|
title: 'Workflow Commands',
|
|
description: 'Project workflow commands for managing development lifecycle',
|
|
},
|
|
{
|
|
src: 'docs/COMMON_GROUND.md',
|
|
dest: 'guides/common-ground.md',
|
|
title: 'Common Ground',
|
|
description: 'Surface and validate hidden assumptions about projects',
|
|
},
|
|
{
|
|
src: 'docs/ATLASSIAN_MCP_SETUP.md',
|
|
dest: 'guides/atlassian-mcp-setup.md',
|
|
title: 'Atlassian MCP Setup',
|
|
description: 'Configure Jira and Confluence MCP integration',
|
|
},
|
|
{
|
|
src: 'docs/local_skill_development.md',
|
|
dest: 'guides/local-development.md',
|
|
title: 'Local Development',
|
|
description: 'Develop and test skills locally',
|
|
},
|
|
{
|
|
src: 'docs/SUPPORTED_AGENTS.md',
|
|
dest: 'guides/supported-agents.md',
|
|
title: 'Supported Agents',
|
|
description: 'Agents and frameworks compatible with the Agent Skills specification',
|
|
},
|
|
];
|
|
|
|
// ─── Link rewrite map (built during sync) ───────────────────────────
|
|
const linkMap = new Map();
|
|
|
|
function buildLinkMap() {
|
|
// Core docs
|
|
for (const { src, dest } of CORE_DOCS) {
|
|
const slug = dest.replace(/\.md$/, '');
|
|
addLinkVariants(src, `${BASE_PATH}/${slug}/`);
|
|
}
|
|
// Guide docs
|
|
for (const { src, dest } of GUIDE_DOCS) {
|
|
const slug = dest.replace(/\.md$/, '');
|
|
addLinkVariants(src, `${BASE_PATH}/${slug}/`);
|
|
// Also map the dest path for cross-references between guides
|
|
const destSlug = dest.replace(/\.md$/, '').replace('guides/', '');
|
|
linkMap.set(`/guides/${destSlug}/`, `${BASE_PATH}/guides/${destSlug}/`);
|
|
}
|
|
// README is now synced via CORE_DOCS, but add extra variant without path prefix
|
|
linkMap.set('README.md', `${BASE_PATH}/readme/`);
|
|
linkMap.set('./README.md', `${BASE_PATH}/readme/`);
|
|
// Workflow cross-links (relative .md references between workflow files)
|
|
const workflowDir = path.join(ROOT, 'docs', 'workflow');
|
|
if (fs.existsSync(workflowDir)) {
|
|
for (const file of fs.readdirSync(workflowDir).filter((f) => f.endsWith('.md'))) {
|
|
const slug = file.replace(/\.md$/, '');
|
|
linkMap.set(file, `${BASE_PATH}/workflows/${slug}/`);
|
|
linkMap.set(`workflow/${file}`, `${BASE_PATH}/workflows/${slug}/`);
|
|
linkMap.set(`/workflows/${slug}/`, `${BASE_PATH}/workflows/${slug}/`);
|
|
}
|
|
}
|
|
// Map common internal paths that may appear without base path
|
|
const coreSlugs = ['getting-started', 'skills-guide', 'readme', 'contributing', 'changelog', 'roadmap'];
|
|
for (const slug of coreSlugs) {
|
|
linkMap.set(`/${slug}/`, `${BASE_PATH}/${slug}/`);
|
|
}
|
|
// Map command source files to their doc site equivalents
|
|
linkMap.set('../commands/common-ground/COMMAND.md', `${BASE_PATH}/guides/common-ground/`);
|
|
linkMap.set('commands/common-ground/COMMAND.md', `${BASE_PATH}/guides/common-ground/`);
|
|
// Map source-only files to GitHub
|
|
linkMap.set('LICENSE', `${GITHUB_BLOB}/LICENSE`);
|
|
linkMap.set('docs/v0.5.0-plan.md', `${GITHUB_BLOB}/docs/v0.5.0-plan.md`);
|
|
}
|
|
|
|
function addLinkVariants(srcPath, siteUrl) {
|
|
const variants = [srcPath, `./${srcPath}`, path.basename(srcPath)];
|
|
for (const v of variants) {
|
|
linkMap.set(v, siteUrl);
|
|
}
|
|
}
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
|
|
function ensureDir(dirPath) {
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
}
|
|
|
|
function stripFrontmatter(content) {
|
|
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
if (match) return { frontmatter: match[1], body: match[2] };
|
|
return { frontmatter: null, body: content };
|
|
}
|
|
|
|
function parseFrontmatter(content) {
|
|
const { frontmatter, body } = stripFrontmatter(content);
|
|
const data = frontmatter ? yaml.load(frontmatter) : {};
|
|
return { data, body };
|
|
}
|
|
|
|
function extractH1(body) {
|
|
const match = body.match(/^#\s+(.+)$/m);
|
|
return match ? match[1].trim() : null;
|
|
}
|
|
|
|
function removeH1(body) {
|
|
return body.replace(/^#\s+.+\n*/m, '');
|
|
}
|
|
|
|
function starlightFrontmatter(fields) {
|
|
const fm = yaml.dump(fields, { lineWidth: -1, quotingType: '"' });
|
|
return `---\n${fm}---\n`;
|
|
}
|
|
|
|
function rewriteLinks(body) {
|
|
// Rewrite markdown links [text](url) using linkMap
|
|
return body.replace(/\[([^\]]*)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
// Skip external URLs and anchors
|
|
if (url.startsWith('http') || url.startsWith('#')) return _match;
|
|
|
|
// Strip anchor from URL for lookup, preserve anchor
|
|
const [urlPath, anchor] = url.split('#');
|
|
const suffix = anchor ? `#${anchor}` : '';
|
|
|
|
// Check linkMap first
|
|
const resolved = linkMap.get(urlPath) || linkMap.get(urlPath.replace(/^\.\//, ''));
|
|
if (resolved) {
|
|
return `[${text}](${resolved}${suffix})`;
|
|
}
|
|
|
|
// Handle absolute paths that need base path prefix
|
|
if (urlPath.startsWith('/') && !urlPath.startsWith(BASE_PATH)) {
|
|
// Check if it's an internal site path (skills, guides, workflows, etc.)
|
|
if (
|
|
urlPath.match(
|
|
/^\/(skills|guides|workflows|getting-started|skills-guide|readme|contributing|changelog|roadmap)\//,
|
|
)
|
|
) {
|
|
return `[${text}](${BASE_PATH}${urlPath}${suffix})`;
|
|
}
|
|
}
|
|
|
|
return _match;
|
|
});
|
|
}
|
|
|
|
function stripHtmlCommentTags(body) {
|
|
// Remove <!-- SKILL_COUNT -->65<!-- /SKILL_COUNT --> style tags, keep inner text
|
|
return body.replace(/<!--\s*\w+\s*-->([^<]+?)<!--\s*\/\w+\s*-->/g, '$1');
|
|
}
|
|
|
|
// ─── Clean synced content ────────────────────────────────────────────
|
|
|
|
function cleanSyncedContent() {
|
|
const syncedDirs = ['skills', 'guides', 'workflows'];
|
|
for (const dir of syncedDirs) {
|
|
const full = path.join(DOCS_DIR, dir);
|
|
if (fs.existsSync(full)) {
|
|
fs.rmSync(full, { recursive: true });
|
|
}
|
|
}
|
|
|
|
const syncedFiles = CORE_DOCS.map((d) => d.dest);
|
|
for (const file of syncedFiles) {
|
|
const full = path.join(DOCS_DIR, file);
|
|
if (fs.existsSync(full)) {
|
|
fs.unlinkSync(full);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Sync core docs ─────────────────────────────────────────────────
|
|
|
|
function syncCoreDocs() {
|
|
for (const { src, dest, title, description, category } of CORE_DOCS) {
|
|
const srcPath = path.join(ROOT, src);
|
|
if (!fs.existsSync(srcPath)) {
|
|
console.warn(` SKIP ${src} (not found)`);
|
|
continue;
|
|
}
|
|
|
|
let content = fs.readFileSync(srcPath, 'utf-8');
|
|
const { body: rawBody } = stripFrontmatter(content);
|
|
let body = removeH1(rawBody);
|
|
body = stripHtmlCommentTags(body);
|
|
body = rewriteLinks(body);
|
|
|
|
// Remove GitHub-specific HTML (badges, images, typing SVGs)
|
|
body = body.replace(/<p align="center">[\s\S]*?<\/p>/g, '');
|
|
|
|
const fm = starlightFrontmatter({ title, description });
|
|
const destPath = path.join(DOCS_DIR, dest);
|
|
ensureDir(path.dirname(destPath));
|
|
fs.writeFileSync(destPath, fm + '\n' + body.trim() + '\n');
|
|
console.log(` ${src} → ${dest}`);
|
|
|
|
const slug = dest.replace(/\.md$/, '');
|
|
pageManifest.push({ siteUrl: `/${slug}/`, title, description, category, contentFile: dest });
|
|
}
|
|
}
|
|
|
|
// ─── Sync guide docs ────────────────────────────────────────────────
|
|
|
|
function syncGuideDocs() {
|
|
for (const { src, dest, title, description } of GUIDE_DOCS) {
|
|
const srcPath = path.join(ROOT, src);
|
|
if (!fs.existsSync(srcPath)) {
|
|
console.warn(` SKIP ${src} (not found)`);
|
|
continue;
|
|
}
|
|
|
|
let content = fs.readFileSync(srcPath, 'utf-8');
|
|
const { body: rawBody } = stripFrontmatter(content);
|
|
let body = removeH1(rawBody);
|
|
body = stripHtmlCommentTags(body);
|
|
body = rewriteLinks(body);
|
|
|
|
const fm = starlightFrontmatter({ title, description });
|
|
const destPath = path.join(DOCS_DIR, dest);
|
|
ensureDir(path.dirname(destPath));
|
|
fs.writeFileSync(destPath, fm + '\n' + body.trim() + '\n');
|
|
console.log(` ${src} → ${dest}`);
|
|
|
|
const slug = dest.replace(/\.md$/, '');
|
|
pageManifest.push({ siteUrl: `/${slug}/`, title, description, category: 'guides', contentFile: dest });
|
|
}
|
|
}
|
|
|
|
// ─── Sync workflow docs ─────────────────────────────────────────────
|
|
|
|
function syncWorkflowDocs() {
|
|
const workflowDir = path.join(ROOT, 'docs', 'workflow');
|
|
if (!fs.existsSync(workflowDir)) {
|
|
console.warn(' SKIP docs/workflow/ (not found)');
|
|
return;
|
|
}
|
|
|
|
const files = fs.readdirSync(workflowDir).filter((f) => f.endsWith('.md'));
|
|
for (const file of files) {
|
|
const srcPath = path.join(workflowDir, file);
|
|
let content = fs.readFileSync(srcPath, 'utf-8');
|
|
const { body: rawBody } = stripFrontmatter(content);
|
|
const h1 = extractH1(rawBody);
|
|
let body = removeH1(rawBody);
|
|
body = stripHtmlCommentTags(body);
|
|
body = rewriteLinks(body);
|
|
|
|
const title =
|
|
h1 ||
|
|
file
|
|
.replace(/\.md$/, '')
|
|
.replace(/-/g, ' ')
|
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
const firstPara = body
|
|
.split('\n\n')
|
|
.find((p) => p.trim() && !p.startsWith('#') && !p.startsWith('|') && !p.startsWith('-'));
|
|
const description = firstPara ? firstPara.trim().replace(/\n/g, ' ').slice(0, 160) : '';
|
|
|
|
const fm = starlightFrontmatter({ title, description });
|
|
const destPath = path.join(DOCS_DIR, 'workflows', file);
|
|
ensureDir(path.dirname(destPath));
|
|
fs.writeFileSync(destPath, fm + '\n' + body.trim() + '\n');
|
|
console.log(` docs/workflow/${file} → workflows/${file}`);
|
|
|
|
const slug = file.replace(/\.md$/, '');
|
|
pageManifest.push({
|
|
siteUrl: `/workflows/${slug}/`,
|
|
title,
|
|
description,
|
|
category: 'workflows',
|
|
contentFile: `workflows/${file}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
// ─── Build skill domain index (for related-skills linking) ──────────
|
|
|
|
function buildSkillIndex() {
|
|
const index = new Map(); // name → { domain, title }
|
|
const skillsDir = path.join(ROOT, 'skills');
|
|
const dirs = fs.readdirSync(skillsDir).filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory());
|
|
|
|
for (const dir of dirs) {
|
|
const skillPath = path.join(skillsDir, dir, 'SKILL.md');
|
|
if (!fs.existsSync(skillPath)) continue;
|
|
|
|
const content = fs.readFileSync(skillPath, 'utf-8');
|
|
const { data, body } = parseFrontmatter(content);
|
|
const domain = data.metadata?.domain || 'specialized';
|
|
const title = extractH1(body) || dir.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
index.set(dir, { domain, title });
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
// ─── Sync skill pages ───────────────────────────────────────────────
|
|
|
|
function syncSkillPages(skillIndex) {
|
|
const skillsDir = path.join(ROOT, 'skills');
|
|
const dirs = fs.readdirSync(skillsDir).filter((d) => fs.statSync(path.join(skillsDir, d)).isDirectory());
|
|
|
|
let count = 0;
|
|
for (const dir of dirs) {
|
|
const skillPath = path.join(skillsDir, dir, 'SKILL.md');
|
|
if (!fs.existsSync(skillPath)) continue;
|
|
|
|
const content = fs.readFileSync(skillPath, 'utf-8');
|
|
const { data, body: rawBody } = parseFrontmatter(content);
|
|
|
|
const domain = data.metadata?.domain || 'specialized';
|
|
const role = data.metadata?.role || 'specialist';
|
|
const scope = data.metadata?.scope || '';
|
|
const outputFormat = data.metadata?.['output-format'] || '';
|
|
const triggers = data.metadata?.triggers || '';
|
|
const relatedSkills = data.metadata?.['related-skills'] || '';
|
|
const description = data.description || '';
|
|
|
|
const h1 = extractH1(rawBody);
|
|
const title = h1 || dir.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
let body = removeH1(rawBody);
|
|
|
|
// Build metadata table
|
|
const metaRows = [];
|
|
if (domain) metaRows.push(`| **Domain** | ${DOMAIN_LABELS[domain] || domain} |`);
|
|
if (role) metaRows.push(`| **Role** | ${role} |`);
|
|
if (scope) metaRows.push(`| **Scope** | ${scope} |`);
|
|
if (outputFormat) metaRows.push(`| **Output** | ${outputFormat} |`);
|
|
|
|
let metaBlock = '';
|
|
if (metaRows.length) {
|
|
metaBlock = `| | |\n|---|---|\n${metaRows.join('\n')}\n\n`;
|
|
}
|
|
|
|
// Triggers
|
|
let triggersBlock = '';
|
|
if (triggers) {
|
|
triggersBlock = `**Triggers:** ${triggers}\n\n`;
|
|
}
|
|
|
|
// Related skills with links
|
|
let relatedBlock = '';
|
|
if (relatedSkills) {
|
|
const names = relatedSkills
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
const links = names.map((name) => {
|
|
const info = skillIndex.get(name);
|
|
if (info) {
|
|
return `[${info.title}](${BASE_PATH}/skills/${info.domain}/${name}/)`;
|
|
}
|
|
return name;
|
|
});
|
|
relatedBlock = `> **Related Skills:** ${links.join(' · ')}\n\n`;
|
|
}
|
|
|
|
// Rewrite reference table links to GitHub blob URLs
|
|
body = body.replace(
|
|
/`references\/([^`]+)`/g,
|
|
(_match, refPath) => `[references/${refPath}](${GITHUB_BLOB}/skills/${dir}/references/${refPath})`,
|
|
);
|
|
|
|
// Strip the canonical documentation backlink: it exists in the source SKILL.md
|
|
// so aggregators (skills.sh, etc.) carry a link back to the docs site, but on
|
|
// the docs site itself the link points at the page rendering it. Self-link.
|
|
body = body.replace(
|
|
/\n+\[Documentation\]\(https:\/\/jeffallan\.github\.io\/claude-skills\/skills\/[^)]+\)\s*$/,
|
|
'',
|
|
);
|
|
|
|
body = rewriteLinks(body);
|
|
|
|
// Assemble frontmatter (branded title + description for SEO, skills only)
|
|
const metaTitle = `Agent Skills for Claude Code | ${title}`;
|
|
const metaDescription = description ? `Claude Skills | ${title} | ${description}` : '';
|
|
const fm = starlightFrontmatter({
|
|
title: metaTitle,
|
|
description: metaDescription,
|
|
sidebar: { label: title },
|
|
});
|
|
|
|
// Assemble page
|
|
const page = fm + '\n' + metaBlock + triggersBlock + relatedBlock + body.trim() + '\n';
|
|
|
|
const destPath = path.join(DOCS_DIR, 'skills', domain, `${dir}.md`);
|
|
ensureDir(path.dirname(destPath));
|
|
fs.writeFileSync(destPath, page);
|
|
count++;
|
|
|
|
pageManifest.push({
|
|
siteUrl: `/skills/${domain}/${dir}/`,
|
|
title,
|
|
description,
|
|
category: `skills:${domain}`,
|
|
contentFile: `skills/${domain}/${dir}.md`,
|
|
});
|
|
}
|
|
|
|
console.log(` ${count} skill pages synced`);
|
|
}
|
|
|
|
// ─── Clean generated public content ──────────────────────────────────
|
|
|
|
function cleanGeneratedPublicContent() {
|
|
if (!fs.existsSync(PUBLIC_DIR)) return;
|
|
|
|
for (const file of ['llms.txt', 'llms-full.txt', 'index.html.md']) {
|
|
const full = path.join(PUBLIC_DIR, file);
|
|
if (fs.existsSync(full)) fs.unlinkSync(full);
|
|
}
|
|
|
|
function cleanDir(dir) {
|
|
if (!fs.existsSync(dir)) return;
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
cleanDir(full);
|
|
try {
|
|
fs.rmdirSync(full);
|
|
} catch {
|
|
/* not empty, skip */
|
|
}
|
|
} else if (entry.name === 'index.html.md') {
|
|
fs.unlinkSync(full);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const dir of [
|
|
'getting-started',
|
|
'skills-guide',
|
|
'skills',
|
|
'guides',
|
|
'workflows',
|
|
'contributing',
|
|
'changelog',
|
|
'roadmap',
|
|
]) {
|
|
cleanDir(path.join(PUBLIC_DIR, dir));
|
|
const full = path.join(PUBLIC_DIR, dir);
|
|
if (fs.existsSync(full)) {
|
|
try {
|
|
fs.rmdirSync(full);
|
|
} catch {
|
|
/* not empty, skip */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Generate markdown mirrors ──────────────────────────────────────
|
|
|
|
function generateMarkdownMirrors() {
|
|
let count = 0;
|
|
for (const { siteUrl, title, contentFile } of pageManifest) {
|
|
const srcPath = path.join(DOCS_DIR, contentFile);
|
|
if (!fs.existsSync(srcPath)) {
|
|
console.warn(` SKIP mirror for ${siteUrl} (${contentFile} not found)`);
|
|
continue;
|
|
}
|
|
|
|
const content = fs.readFileSync(srcPath, 'utf-8');
|
|
const { body } = stripFrontmatter(content);
|
|
const markdown = `# ${title}\n\n${body.trim()}\n`;
|
|
|
|
const destPath = path.join(PUBLIC_DIR, siteUrl, 'index.html.md');
|
|
ensureDir(path.dirname(destPath));
|
|
fs.writeFileSync(destPath, markdown);
|
|
count++;
|
|
}
|
|
console.log(` ${count} markdown mirrors generated`);
|
|
}
|
|
|
|
// ─── Generate index mirror (landing page) ───────────────────────────
|
|
|
|
function generateIndexMirror() {
|
|
const versionData = JSON.parse(fs.readFileSync(path.join(ROOT, 'version.json'), 'utf-8'));
|
|
|
|
const markdown = `# Claude Skills
|
|
|
|
> ${versionData.skillCount} specialized skills for Claude Code — progressive disclosure, context engineering, and full-stack coverage.
|
|
|
|
Transform Claude Code into your expert pair programmer across the entire development stack.
|
|
|
|
## Quick Install
|
|
|
|
\`\`\`bash
|
|
/plugin marketplace add jeffallan/claude-skills
|
|
\`\`\`
|
|
|
|
\`\`\`bash
|
|
/plugin install fullstack-dev-skills@jeffallan
|
|
\`\`\`
|
|
|
|
## Stats
|
|
|
|
- **${versionData.skillCount}** specialized skills across 12 domains
|
|
- **${versionData.workflowCount}** project workflow commands
|
|
- **${versionData.referenceFileCount}** deep-dive reference files
|
|
|
|
See the [Getting Started guide](/getting-started/) for all installation methods and first steps.
|
|
`;
|
|
|
|
const destPath = path.join(PUBLIC_DIR, 'index.html.md');
|
|
ensureDir(PUBLIC_DIR);
|
|
fs.writeFileSync(destPath, markdown);
|
|
console.log(' index.html.md generated');
|
|
}
|
|
|
|
// ─── Generate llms.txt ──────────────────────────────────────────────
|
|
|
|
function generateLlmsTxt() {
|
|
const versionData = JSON.parse(fs.readFileSync(path.join(ROOT, 'version.json'), 'utf-8'));
|
|
|
|
const lines = [];
|
|
lines.push('# Claude Skills');
|
|
lines.push(
|
|
`> ${versionData.skillCount} specialized skills for Claude Code — progressive disclosure, context engineering, and full-stack coverage.`,
|
|
);
|
|
lines.push('');
|
|
|
|
// Group manifest entries by category
|
|
const groups = {};
|
|
for (const entry of pageManifest) {
|
|
if (!groups[entry.category]) groups[entry.category] = [];
|
|
groups[entry.category].push(entry);
|
|
}
|
|
|
|
// Docs
|
|
if (groups['docs']?.length) {
|
|
lines.push('## Docs');
|
|
for (const { siteUrl, title, description } of groups['docs']) {
|
|
lines.push(`- [${title}](${siteUrl}index.html.md): ${description}`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
|
|
// Guides
|
|
if (groups['guides']?.length) {
|
|
lines.push('## Guides');
|
|
for (const { siteUrl, title, description } of groups['guides']) {
|
|
lines.push(`- [${title}](${siteUrl}index.html.md): ${description}`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
|
|
// Skills by domain (same order as DOMAIN_LABELS)
|
|
for (const [domain, label] of Object.entries(DOMAIN_LABELS)) {
|
|
const key = `skills:${domain}`;
|
|
if (groups[key]?.length) {
|
|
lines.push(`## Skills: ${label}`);
|
|
for (const { siteUrl, title, description } of groups[key]) {
|
|
lines.push(`- [${title}](${siteUrl}index.html.md): ${description}`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
}
|
|
|
|
// Workflows
|
|
if (groups['workflows']?.length) {
|
|
lines.push('## Workflows');
|
|
for (const { siteUrl, title, description } of groups['workflows']) {
|
|
lines.push(`- [${title}](${siteUrl}index.html.md): ${description}`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
|
|
// Optional (project pages)
|
|
if (groups['project']?.length) {
|
|
lines.push('## Optional');
|
|
for (const { siteUrl, title, description } of groups['project']) {
|
|
lines.push(`- [${title}](${siteUrl}index.html.md): ${description}`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
|
|
const destPath = path.join(PUBLIC_DIR, 'llms.txt');
|
|
ensureDir(PUBLIC_DIR);
|
|
fs.writeFileSync(destPath, lines.join('\n'));
|
|
console.log(' llms.txt generated');
|
|
}
|
|
|
|
// ─── Generate llms-full.txt ─────────────────────────────────────────
|
|
|
|
function generateLlmsFullTxt() {
|
|
const sections = [];
|
|
|
|
// Order: docs → guides → skills (by domain) → workflows → project
|
|
const orderedCategories = [
|
|
'docs',
|
|
'guides',
|
|
...Object.keys(DOMAIN_LABELS).map((d) => `skills:${d}`),
|
|
'workflows',
|
|
'project',
|
|
];
|
|
|
|
const groups = {};
|
|
for (const entry of pageManifest) {
|
|
if (!groups[entry.category]) groups[entry.category] = [];
|
|
groups[entry.category].push(entry);
|
|
}
|
|
|
|
for (const cat of orderedCategories) {
|
|
if (!groups[cat]?.length) continue;
|
|
for (const { title, contentFile } of groups[cat]) {
|
|
const srcPath = path.join(DOCS_DIR, contentFile);
|
|
if (!fs.existsSync(srcPath)) continue;
|
|
|
|
const content = fs.readFileSync(srcPath, 'utf-8');
|
|
const { body } = stripFrontmatter(content);
|
|
sections.push(`# ${title}\n\n${body.trim()}`);
|
|
}
|
|
}
|
|
|
|
const destPath = path.join(PUBLIC_DIR, 'llms-full.txt');
|
|
ensureDir(PUBLIC_DIR);
|
|
fs.writeFileSync(destPath, sections.join('\n\n---\n\n') + '\n');
|
|
console.log(' llms-full.txt generated');
|
|
}
|
|
|
|
// ─── Main ────────────────────────────────────────────────────────────
|
|
|
|
function main() {
|
|
console.log('sync-content: starting...');
|
|
|
|
buildLinkMap();
|
|
|
|
console.log('Cleaning synced content...');
|
|
cleanSyncedContent();
|
|
|
|
console.log('Syncing core docs...');
|
|
syncCoreDocs();
|
|
|
|
console.log('Syncing guide docs...');
|
|
syncGuideDocs();
|
|
|
|
console.log('Syncing workflow docs...');
|
|
syncWorkflowDocs();
|
|
|
|
console.log('Building skill index...');
|
|
const skillIndex = buildSkillIndex();
|
|
|
|
console.log('Syncing skill pages...');
|
|
syncSkillPages(skillIndex);
|
|
|
|
console.log('Generating LLM content...');
|
|
cleanGeneratedPublicContent();
|
|
generateMarkdownMirrors();
|
|
generateIndexMirror();
|
|
generateLlmsTxt();
|
|
generateLlmsFullTxt();
|
|
|
|
console.log('sync-content: done.');
|
|
}
|
|
|
|
main();
|