mirror of
https://github.com/Jeffallan/claude-skills.git
synced 2026-09-14 18:43:38 +08:00
WIP adds doc site
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export default new Map();
|
||||
@@ -0,0 +1 @@
|
||||
export default new Map();
|
||||
Vendored
+199
@@ -0,0 +1,199 @@
|
||||
declare module 'astro:content' {
|
||||
export interface RenderResult {
|
||||
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
|
||||
headings: import('astro').MarkdownHeading[];
|
||||
remarkPluginFrontmatter: Record<string, any>;
|
||||
}
|
||||
interface Render {
|
||||
'.md': Promise<RenderResult>;
|
||||
}
|
||||
|
||||
export interface RenderedContent {
|
||||
html: string;
|
||||
metadata?: {
|
||||
imagePaths: Array<string>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'astro:content' {
|
||||
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
|
||||
|
||||
export type CollectionKey = keyof AnyEntryMap;
|
||||
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
|
||||
|
||||
export type ContentCollectionKey = keyof ContentEntryMap;
|
||||
export type DataCollectionKey = keyof DataEntryMap;
|
||||
|
||||
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
|
||||
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
|
||||
ContentEntryMap[C]
|
||||
>['slug'];
|
||||
|
||||
export type ReferenceDataEntry<
|
||||
C extends CollectionKey,
|
||||
E extends keyof DataEntryMap[C] = string,
|
||||
> = {
|
||||
collection: C;
|
||||
id: E;
|
||||
};
|
||||
export type ReferenceContentEntry<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}) = string,
|
||||
> = {
|
||||
collection: C;
|
||||
slug: E;
|
||||
};
|
||||
export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
|
||||
collection: C;
|
||||
id: string;
|
||||
};
|
||||
|
||||
/** @deprecated Use `getEntry` instead. */
|
||||
export function getEntryBySlug<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
// Note that this has to accept a regular string too, for SSR
|
||||
entrySlug: E,
|
||||
): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
|
||||
/** @deprecated Use `getEntry` instead. */
|
||||
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
|
||||
collection: C,
|
||||
entryId: E,
|
||||
): Promise<CollectionEntry<C>>;
|
||||
|
||||
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
|
||||
collection: C,
|
||||
filter?: (entry: CollectionEntry<C>) => entry is E,
|
||||
): Promise<E[]>;
|
||||
export function getCollection<C extends keyof AnyEntryMap>(
|
||||
collection: C,
|
||||
filter?: (entry: CollectionEntry<C>) => unknown,
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
|
||||
export function getLiveCollection<C extends keyof LiveContentConfig['collections']>(
|
||||
collection: C,
|
||||
filter?: LiveLoaderCollectionFilterType<C>,
|
||||
): Promise<
|
||||
import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
|
||||
>;
|
||||
|
||||
export function getEntry<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(
|
||||
entry: ReferenceContentEntry<C, E>,
|
||||
): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof DataEntryMap,
|
||||
E extends keyof DataEntryMap[C] | (string & {}),
|
||||
>(
|
||||
entry: ReferenceDataEntry<C, E>,
|
||||
): E extends keyof DataEntryMap[C]
|
||||
? Promise<DataEntryMap[C][E]>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof ContentEntryMap,
|
||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
slug: E,
|
||||
): E extends ValidContentEntrySlug<C>
|
||||
? Promise<CollectionEntry<C>>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof DataEntryMap,
|
||||
E extends keyof DataEntryMap[C] | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
id: E,
|
||||
): E extends keyof DataEntryMap[C]
|
||||
? string extends keyof DataEntryMap[C]
|
||||
? Promise<DataEntryMap[C][E]> | undefined
|
||||
: Promise<DataEntryMap[C][E]>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getLiveEntry<C extends keyof LiveContentConfig['collections']>(
|
||||
collection: C,
|
||||
filter: string | LiveLoaderEntryFilterType<C>,
|
||||
): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
|
||||
|
||||
/** Resolve an array of entry references from the same collection */
|
||||
export function getEntries<C extends keyof ContentEntryMap>(
|
||||
entries: ReferenceContentEntry<C, ValidContentEntrySlug<C>>[],
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
export function getEntries<C extends keyof DataEntryMap>(
|
||||
entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
|
||||
export function render<C extends keyof AnyEntryMap>(
|
||||
entry: AnyEntryMap[C][string],
|
||||
): Promise<RenderResult>;
|
||||
|
||||
export function reference<C extends keyof AnyEntryMap>(
|
||||
collection: C,
|
||||
): import('astro/zod').ZodEffects<
|
||||
import('astro/zod').ZodString,
|
||||
C extends keyof ContentEntryMap
|
||||
? ReferenceContentEntry<C, ValidContentEntrySlug<C>>
|
||||
: ReferenceDataEntry<C, keyof DataEntryMap[C]>
|
||||
>;
|
||||
// Allow generic `string` to avoid excessive type errors in the config
|
||||
// if `dev` is not running to update as you edit.
|
||||
// Invalid collection names will be caught at build time.
|
||||
export function reference<C extends string>(
|
||||
collection: C,
|
||||
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
|
||||
|
||||
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
|
||||
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
|
||||
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
|
||||
>;
|
||||
|
||||
type ContentEntryMap = {
|
||||
|
||||
};
|
||||
|
||||
type DataEntryMap = {
|
||||
|
||||
};
|
||||
|
||||
type AnyEntryMap = ContentEntryMap & DataEntryMap;
|
||||
|
||||
type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
|
||||
infer TData,
|
||||
infer TEntryFilter,
|
||||
infer TCollectionFilter,
|
||||
infer TError
|
||||
>
|
||||
? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
|
||||
: { data: never; entryFilter: never; collectionFilter: never; error: never };
|
||||
type ExtractDataType<T> = ExtractLoaderTypes<T>['data'];
|
||||
type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
|
||||
type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
|
||||
type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
|
||||
|
||||
type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
|
||||
LiveContentConfig['collections'][C]['schema'] extends undefined
|
||||
? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
|
||||
: import('astro/zod').infer<
|
||||
Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
|
||||
>;
|
||||
type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
|
||||
ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
|
||||
type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
|
||||
ExtractCollectionFilterType<LiveContentConfig['collections'][C]['loader']>;
|
||||
type LiveLoaderErrorType<C extends keyof LiveContentConfig['collections']> = ExtractErrorType<
|
||||
LiveContentConfig['collections'][C]['loader']
|
||||
>;
|
||||
|
||||
export type ContentConfig = typeof import("../src/content.config.mjs");
|
||||
export type LiveContentConfig = never;
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
/// <reference types="astro/client" />
|
||||
/// <reference path="content.d.ts" />
|
||||
+13
@@ -34,3 +34,16 @@ yarn-error.log*
|
||||
|
||||
# Awesome list submission workspace
|
||||
submits/
|
||||
|
||||
# Astro docs site
|
||||
site/node_modules/
|
||||
site/dist/
|
||||
site/.astro/
|
||||
site/src/content/docs/skills/
|
||||
site/src/content/docs/guides/
|
||||
site/src/content/docs/workflows/
|
||||
site/src/content/docs/getting-started.md
|
||||
site/src/content/docs/skills-guide.md
|
||||
site/src/content/docs/contributing.md
|
||||
site/src/content/docs/changelog.md
|
||||
site/src/content/docs/roadmap.md
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: dev-link dev-unlink validate test
|
||||
.PHONY: dev-link dev-unlink validate test site-dev site-build
|
||||
|
||||
PLUGIN_NAME := fullstack-dev-skills
|
||||
VERSION := $(shell python -c "import json; print(json.load(open('version.json'))['version'])")
|
||||
@@ -47,3 +47,9 @@ validate:
|
||||
|
||||
test:
|
||||
bash scripts/test-makefile.sh
|
||||
|
||||
site-dev:
|
||||
cd site && npm run dev
|
||||
|
||||
site-build:
|
||||
cd site && npm run build
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'Claude Skills',
|
||||
description:
|
||||
'65 specialized skills for Claude Code — progressive disclosure, context engineering, and full-stack coverage.',
|
||||
customCss: ['./src/styles/custom.css'],
|
||||
social: [
|
||||
{
|
||||
icon: 'github',
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/jeffallan/claude-skills',
|
||||
},
|
||||
],
|
||||
sidebar: [
|
||||
{ label: 'Home', link: '/' },
|
||||
{ label: 'Getting Started', link: '/getting-started/' },
|
||||
{ label: 'Skills Guide', link: '/skills-guide/' },
|
||||
{
|
||||
label: 'Guides',
|
||||
items: [
|
||||
{ label: 'Workflow Commands', link: '/guides/workflow-commands/' },
|
||||
{ label: 'Common Ground', link: '/guides/common-ground/' },
|
||||
{
|
||||
label: 'Atlassian MCP Setup',
|
||||
link: '/guides/atlassian-mcp-setup/',
|
||||
},
|
||||
{
|
||||
label: 'Local Development',
|
||||
link: '/guides/local-development/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Workflows',
|
||||
collapsed: true,
|
||||
autogenerate: { directory: 'workflows' },
|
||||
},
|
||||
{
|
||||
label: 'Language',
|
||||
collapsed: true,
|
||||
autogenerate: { directory: 'skills/language' },
|
||||
},
|
||||
{
|
||||
label: 'Backend Frameworks',
|
||||
collapsed: true,
|
||||
autogenerate: { directory: 'skills/backend' },
|
||||
},
|
||||
{
|
||||
label: 'Frontend & Mobile',
|
||||
collapsed: true,
|
||||
autogenerate: { directory: 'skills/frontend' },
|
||||
},
|
||||
{
|
||||
label: 'Infrastructure & Cloud',
|
||||
collapsed: true,
|
||||
autogenerate: { directory: 'skills/infrastructure' },
|
||||
},
|
||||
{
|
||||
label: 'API & Architecture',
|
||||
collapsed: true,
|
||||
autogenerate: { directory: 'skills/api-architecture' },
|
||||
},
|
||||
{
|
||||
label: 'Quality & Testing',
|
||||
collapsed: true,
|
||||
autogenerate: { directory: 'skills/quality' },
|
||||
},
|
||||
{
|
||||
label: 'DevOps & Operations',
|
||||
collapsed: true,
|
||||
autogenerate: { directory: 'skills/devops' },
|
||||
},
|
||||
{
|
||||
label: 'Security',
|
||||
collapsed: true,
|
||||
autogenerate: { directory: 'skills/security' },
|
||||
},
|
||||
{
|
||||
label: 'Data & ML',
|
||||
collapsed: true,
|
||||
autogenerate: { directory: 'skills/data-ml' },
|
||||
},
|
||||
{
|
||||
label: 'Platform',
|
||||
collapsed: true,
|
||||
autogenerate: { directory: 'skills/platform' },
|
||||
},
|
||||
{
|
||||
label: 'Specialized',
|
||||
collapsed: true,
|
||||
autogenerate: { directory: 'skills/specialized' },
|
||||
},
|
||||
{
|
||||
label: 'Workflow Skills',
|
||||
collapsed: true,
|
||||
autogenerate: { directory: 'skills/workflow' },
|
||||
},
|
||||
{
|
||||
label: 'Project',
|
||||
items: [
|
||||
{ label: 'Contributing', link: '/contributing/' },
|
||||
{ label: 'Changelog', link: '/changelog/' },
|
||||
{ label: 'Roadmap', link: '/roadmap/' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
Generated
+6857
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "claude-skills-docs",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"sync": "node scripts/sync-content.mjs",
|
||||
"dev": "npm run sync && astro dev",
|
||||
"build": "npm run sync && astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^5.7.10",
|
||||
"@astrojs/starlight": "^0.34.1",
|
||||
"sharp": "^0.33.5",
|
||||
"js-yaml": "^4.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
#!/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 GITHUB_BLOB = 'https://github.com/jeffallan/claude-skills/blob/main';
|
||||
|
||||
// ─── 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',
|
||||
};
|
||||
|
||||
// ─── Role → sidebar badge ───────────────────────────────────────────
|
||||
const ROLE_BADGES = {
|
||||
specialist: { text: 'Specialist', variant: 'default' },
|
||||
expert: { text: 'Expert', variant: 'success' },
|
||||
architect: { text: 'Architect', variant: 'caution' },
|
||||
engineer: { text: 'Engineer', variant: 'note' },
|
||||
};
|
||||
|
||||
// ─── 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' },
|
||||
{ src: 'SKILLS_GUIDE.md', dest: 'skills-guide.md', title: 'Skills Guide' },
|
||||
{ src: 'CONTRIBUTING.md', dest: 'contributing.md', title: 'Contributing' },
|
||||
{ src: 'CHANGELOG.md', dest: 'changelog.md', title: 'Changelog' },
|
||||
{ src: 'ROADMAP.md', dest: 'roadmap.md', title: 'Roadmap' },
|
||||
];
|
||||
|
||||
// ─── Guide docs mapping ─────────────────────────────────────────────
|
||||
const GUIDE_DOCS = [
|
||||
{ src: 'docs/WORKFLOW_COMMANDS.md', dest: 'guides/workflow-commands.md', title: 'Workflow Commands' },
|
||||
{ src: 'docs/COMMON_GROUND.md', dest: 'guides/common-ground.md', title: 'Common Ground' },
|
||||
{ src: 'docs/ATLASSIAN_MCP_SETUP.md', dest: 'guides/atlassian-mcp-setup.md', title: 'Atlassian MCP Setup' },
|
||||
{ src: 'docs/local_skill_development.md', dest: 'guides/local-development.md', title: 'Local Development' },
|
||||
];
|
||||
|
||||
// ─── 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, `/${slug}/`);
|
||||
}
|
||||
// Guide docs
|
||||
for (const { src, dest } of GUIDE_DOCS) {
|
||||
const slug = dest.replace(/\.md$/, '');
|
||||
addLinkVariants(src, `/${slug}/`);
|
||||
}
|
||||
}
|
||||
|
||||
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 resolved = linkMap.get(urlPath) || linkMap.get(urlPath.replace(/^\.\//, ''));
|
||||
if (resolved) {
|
||||
const suffix = anchor ? `#${anchor}` : '';
|
||||
return `[${text}](${resolved}${suffix})`;
|
||||
}
|
||||
return _match;
|
||||
});
|
||||
}
|
||||
|
||||
function stripHtmlCommentTags(body) {
|
||||
// Remove <!-- SKILL_COUNT -->65<!-- /SKILL_COUNT --> style tags, keep inner text
|
||||
return body.replace(/<!--\s*\w+\s*-->(\w+)<!--\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 } 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 });
|
||||
const destPath = path.join(DOCS_DIR, dest);
|
||||
ensureDir(path.dirname(destPath));
|
||||
fs.writeFileSync(destPath, fm + '\n' + body.trim() + '\n');
|
||||
console.log(` ${src} → ${dest}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Sync guide docs ────────────────────────────────────────────────
|
||||
|
||||
function syncGuideDocs() {
|
||||
for (const { src, dest, title } 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 });
|
||||
const destPath = path.join(DOCS_DIR, dest);
|
||||
ensureDir(path.dirname(destPath));
|
||||
fs.writeFileSync(destPath, fm + '\n' + body.trim() + '\n');
|
||||
console.log(` ${src} → ${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 fm = starlightFrontmatter({ title });
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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}](/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})`
|
||||
);
|
||||
|
||||
body = rewriteLinks(body);
|
||||
|
||||
// Build sidebar badge
|
||||
const badge = ROLE_BADGES[role] || ROLE_BADGES.specialist;
|
||||
|
||||
// Assemble frontmatter
|
||||
const fm = starlightFrontmatter({
|
||||
title,
|
||||
description,
|
||||
sidebar: { badge },
|
||||
});
|
||||
|
||||
// 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++;
|
||||
}
|
||||
|
||||
console.log(` ${count} skill pages synced`);
|
||||
}
|
||||
|
||||
// ─── 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('sync-content: done.');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { docsLoader } from '@astrojs/starlight/loaders';
|
||||
import { docsSchema } from '@astrojs/starlight/schema';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Claude Skills
|
||||
description: 65 specialized skills for Claude Code — progressive disclosure, context engineering, and full-stack coverage.
|
||||
template: splash
|
||||
hero:
|
||||
tagline: Transform Claude Code into your expert pair programmer across the entire development stack.
|
||||
actions:
|
||||
- text: Get Started
|
||||
link: /getting-started/
|
||||
icon: right-arrow
|
||||
variant: primary
|
||||
- text: Browse Skills
|
||||
link: /skills-guide/
|
||||
variant: minimal
|
||||
---
|
||||
|
||||
import { Card, CardGrid } from '@astrojs/starlight/components';
|
||||
|
||||
<CardGrid stagger>
|
||||
<Card title="65 Skills" icon="puzzle">
|
||||
Specialized skills covering 12 domains — languages, frameworks, infrastructure, security, DevOps, data/ML, and more.
|
||||
</Card>
|
||||
<Card title="9 Workflows" icon="rocket">
|
||||
Project workflow commands managing epics from discovery through retrospectives, with Jira and Confluence integration.
|
||||
</Card>
|
||||
<Card title="357 References" icon="open-book">
|
||||
Deep-dive reference files loaded on-demand for surgical precision when context requires it.
|
||||
</Card>
|
||||
<Card title="Progressive Disclosure" icon="magnifier">
|
||||
Lean 80-line skill cores with routing tables to detailed references — 50% token reduction, faster initial responses.
|
||||
</Card>
|
||||
</CardGrid>
|
||||
|
||||
## Quick Install
|
||||
|
||||
```bash
|
||||
/plugin marketplace add jeffallan/claude-skills
|
||||
```
|
||||
|
||||
```bash
|
||||
/plugin install fullstack-dev-skills@jeffallan
|
||||
```
|
||||
|
||||
See the [Getting Started guide](/getting-started/) for all installation methods and first steps.
|
||||
@@ -0,0 +1,40 @@
|
||||
/* Purple-to-cyan theme matching social preview */
|
||||
|
||||
:root {
|
||||
--sl-color-accent-low: #ede9fe;
|
||||
--sl-color-accent: #7c3aed;
|
||||
--sl-color-accent-high: #4c1d95;
|
||||
--sl-color-white: #1a1a2e;
|
||||
--sl-color-gray-1: #3d3d5c;
|
||||
--sl-color-gray-2: #53536e;
|
||||
--sl-color-gray-3: #8888a0;
|
||||
--sl-color-gray-4: #b8b8cc;
|
||||
--sl-color-gray-5: #d4d4e0;
|
||||
--sl-color-gray-6: #ededf4;
|
||||
--sl-color-black: #fafafe;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
--sl-color-accent-low: #1e1048;
|
||||
--sl-color-accent: #8b5cf6;
|
||||
--sl-color-accent-high: #c4b5fd;
|
||||
--sl-color-white: #fafafe;
|
||||
--sl-color-gray-1: #ededf4;
|
||||
--sl-color-gray-2: #d4d4e0;
|
||||
--sl-color-gray-3: #8888a0;
|
||||
--sl-color-gray-4: #53536e;
|
||||
--sl-color-gray-5: #3d3d5c;
|
||||
--sl-color-gray-6: #1a1a2e;
|
||||
--sl-color-black: #0f0f1a;
|
||||
}
|
||||
|
||||
/* Gradient accent bar under site header */
|
||||
header.header {
|
||||
border-bottom: 2px solid transparent;
|
||||
border-image: linear-gradient(90deg, #7c3aed, #6366f1, #3b82f6, #0ea5e9) 1;
|
||||
}
|
||||
|
||||
/* Skill metadata tables: tighter layout */
|
||||
.sl-markdown-content table:first-of-type {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
||||
Reference in New Issue
Block a user