mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 02:15:12 +00:00
feat: Add GitHub Pages website for browsing resources
- Add static website with pages for agents, prompts, instructions, skills, and collections - Implement client-side fuzzy search across all resources - Add file viewer modal with copy-to-clipboard and install-to-editor functionality - Add Tools page for MCP server and future tools - Add Samples page placeholder for copilot-sdk cookbook migration - Add metadata JSON generation script (eng/generate-website-data.mjs) - Add GitHub Actions workflow for automated Pages deployment - Update package.json with website build scripts
This commit is contained in:
392
eng/generate-website-data.mjs
Normal file
392
eng/generate-website-data.mjs
Normal file
@@ -0,0 +1,392 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generate JSON metadata files for the GitHub Pages website.
|
||||
* This script extracts metadata from agents, prompts, instructions, skills, and collections
|
||||
* and writes them to website/data/ for client-side search and display.
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname } from "path";
|
||||
import {
|
||||
AGENTS_DIR,
|
||||
INSTRUCTIONS_DIR,
|
||||
PROMPTS_DIR,
|
||||
SKILLS_DIR,
|
||||
COLLECTIONS_DIR,
|
||||
ROOT_FOLDER,
|
||||
} from "./constants.mjs";
|
||||
import {
|
||||
parseFrontmatter,
|
||||
parseCollectionYaml,
|
||||
parseSkillMetadata,
|
||||
} from "./yaml-parser.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const WEBSITE_DATA_DIR = path.join(ROOT_FOLDER, "website", "data");
|
||||
|
||||
/**
|
||||
* Ensure the output directory exists
|
||||
*/
|
||||
function ensureDataDir() {
|
||||
if (!fs.existsSync(WEBSITE_DATA_DIR)) {
|
||||
fs.mkdirSync(WEBSITE_DATA_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract title from filename or frontmatter
|
||||
*/
|
||||
function extractTitle(filePath, frontmatter) {
|
||||
if (frontmatter?.title) return frontmatter.title;
|
||||
if (frontmatter?.name) {
|
||||
return frontmatter.name
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
// Fallback to filename
|
||||
const basename = path.basename(filePath);
|
||||
const name = basename
|
||||
.replace(/\.(agent|prompt|instructions)\.md$/, "")
|
||||
.replace(/\.md$/, "");
|
||||
return name
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file content (for preview/full content)
|
||||
*/
|
||||
function getFileContent(filePath) {
|
||||
try {
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate agents metadata
|
||||
*/
|
||||
function generateAgentsData() {
|
||||
const agents = [];
|
||||
const files = fs
|
||||
.readdirSync(AGENTS_DIR)
|
||||
.filter((f) => f.endsWith(".agent.md"));
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(AGENTS_DIR, file);
|
||||
const frontmatter = parseFrontmatter(filePath);
|
||||
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
|
||||
|
||||
agents.push({
|
||||
id: file.replace(".agent.md", ""),
|
||||
title: extractTitle(filePath, frontmatter),
|
||||
description: frontmatter?.description || "",
|
||||
model: frontmatter?.model || null,
|
||||
tools: frontmatter?.tools || [],
|
||||
mcpServers: frontmatter?.["mcp-servers"]
|
||||
? Object.keys(frontmatter["mcp-servers"])
|
||||
: [],
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
|
||||
return agents.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate prompts metadata
|
||||
*/
|
||||
function generatePromptsData() {
|
||||
const prompts = [];
|
||||
const files = fs
|
||||
.readdirSync(PROMPTS_DIR)
|
||||
.filter((f) => f.endsWith(".prompt.md"));
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(PROMPTS_DIR, file);
|
||||
const frontmatter = parseFrontmatter(filePath);
|
||||
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
|
||||
|
||||
prompts.push({
|
||||
id: file.replace(".prompt.md", ""),
|
||||
title: extractTitle(filePath, frontmatter),
|
||||
description: frontmatter?.description || "",
|
||||
agent: frontmatter?.agent || null,
|
||||
model: frontmatter?.model || null,
|
||||
tools: frontmatter?.tools || [],
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
|
||||
return prompts.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate instructions metadata
|
||||
*/
|
||||
function generateInstructionsData() {
|
||||
const instructions = [];
|
||||
const files = fs
|
||||
.readdirSync(INSTRUCTIONS_DIR)
|
||||
.filter((f) => f.endsWith(".instructions.md"));
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(INSTRUCTIONS_DIR, file);
|
||||
const frontmatter = parseFrontmatter(filePath);
|
||||
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
|
||||
|
||||
instructions.push({
|
||||
id: file.replace(".instructions.md", ""),
|
||||
title: extractTitle(filePath, frontmatter),
|
||||
description: frontmatter?.description || "",
|
||||
applyTo: frontmatter?.applyTo || null,
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
|
||||
return instructions.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate skills metadata
|
||||
*/
|
||||
function generateSkillsData() {
|
||||
const skills = [];
|
||||
|
||||
if (!fs.existsSync(SKILLS_DIR)) {
|
||||
return skills;
|
||||
}
|
||||
|
||||
const folders = fs
|
||||
.readdirSync(SKILLS_DIR)
|
||||
.filter((f) => fs.statSync(path.join(SKILLS_DIR, f)).isDirectory());
|
||||
|
||||
for (const folder of folders) {
|
||||
const skillPath = path.join(SKILLS_DIR, folder);
|
||||
const metadata = parseSkillMetadata(skillPath);
|
||||
|
||||
if (metadata) {
|
||||
const relativePath = path.relative(ROOT_FOLDER, skillPath).replace(/\\/g, "/");
|
||||
|
||||
skills.push({
|
||||
id: folder,
|
||||
name: metadata.name,
|
||||
title: metadata.name
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" "),
|
||||
description: metadata.description,
|
||||
assets: metadata.assets,
|
||||
path: relativePath,
|
||||
skillFile: `${relativePath}/SKILL.md`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return skills.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate collections metadata
|
||||
*/
|
||||
function generateCollectionsData() {
|
||||
const collections = [];
|
||||
|
||||
if (!fs.existsSync(COLLECTIONS_DIR)) {
|
||||
return collections;
|
||||
}
|
||||
|
||||
const files = fs
|
||||
.readdirSync(COLLECTIONS_DIR)
|
||||
.filter((f) => f.endsWith(".collection.yml"));
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(COLLECTIONS_DIR, file);
|
||||
const data = parseCollectionYaml(filePath);
|
||||
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
|
||||
|
||||
if (data) {
|
||||
collections.push({
|
||||
id: file.replace(".collection.yml", ""),
|
||||
name: data.name || file.replace(".collection.yml", ""),
|
||||
description: data.description || "",
|
||||
tags: data.tags || [],
|
||||
featured: data.featured || false,
|
||||
items: (data.items || []).map((item) => ({
|
||||
path: item.path,
|
||||
kind: item.kind,
|
||||
usage: item.usage || null,
|
||||
})),
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort with featured first, then alphabetically
|
||||
return collections.sort((a, b) => {
|
||||
if (a.featured && !b.featured) return -1;
|
||||
if (!a.featured && b.featured) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a combined index for search
|
||||
*/
|
||||
function generateSearchIndex(agents, prompts, instructions, skills, collections) {
|
||||
const index = [];
|
||||
|
||||
for (const agent of agents) {
|
||||
index.push({
|
||||
type: "agent",
|
||||
id: agent.id,
|
||||
title: agent.title,
|
||||
description: agent.description,
|
||||
path: agent.path,
|
||||
searchText: `${agent.title} ${agent.description} ${agent.tools.join(" ")}`.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
for (const prompt of prompts) {
|
||||
index.push({
|
||||
type: "prompt",
|
||||
id: prompt.id,
|
||||
title: prompt.title,
|
||||
description: prompt.description,
|
||||
path: prompt.path,
|
||||
searchText: `${prompt.title} ${prompt.description}`.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
for (const instruction of instructions) {
|
||||
index.push({
|
||||
type: "instruction",
|
||||
id: instruction.id,
|
||||
title: instruction.title,
|
||||
description: instruction.description,
|
||||
path: instruction.path,
|
||||
searchText: `${instruction.title} ${instruction.description} ${instruction.applyTo || ""}`.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
for (const skill of skills) {
|
||||
index.push({
|
||||
type: "skill",
|
||||
id: skill.id,
|
||||
title: skill.title,
|
||||
description: skill.description,
|
||||
path: skill.path,
|
||||
searchText: `${skill.title} ${skill.description}`.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
for (const collection of collections) {
|
||||
index.push({
|
||||
type: "collection",
|
||||
id: collection.id,
|
||||
title: collection.name,
|
||||
description: collection.description,
|
||||
path: collection.path,
|
||||
tags: collection.tags,
|
||||
searchText: `${collection.name} ${collection.description} ${collection.tags.join(" ")}`.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
async function main() {
|
||||
console.log("Generating website data...\n");
|
||||
|
||||
ensureDataDir();
|
||||
|
||||
// Generate all data
|
||||
const agents = generateAgentsData();
|
||||
console.log(`✓ Generated ${agents.length} agents`);
|
||||
|
||||
const prompts = generatePromptsData();
|
||||
console.log(`✓ Generated ${prompts.length} prompts`);
|
||||
|
||||
const instructions = generateInstructionsData();
|
||||
console.log(`✓ Generated ${instructions.length} instructions`);
|
||||
|
||||
const skills = generateSkillsData();
|
||||
console.log(`✓ Generated ${skills.length} skills`);
|
||||
|
||||
const collections = generateCollectionsData();
|
||||
console.log(`✓ Generated ${collections.length} collections`);
|
||||
|
||||
const searchIndex = generateSearchIndex(agents, prompts, instructions, skills, collections);
|
||||
console.log(`✓ Generated search index with ${searchIndex.length} items`);
|
||||
|
||||
// Write JSON files
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "agents.json"),
|
||||
JSON.stringify(agents, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "prompts.json"),
|
||||
JSON.stringify(prompts, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "instructions.json"),
|
||||
JSON.stringify(instructions, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "skills.json"),
|
||||
JSON.stringify(skills, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "collections.json"),
|
||||
JSON.stringify(collections, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "search-index.json"),
|
||||
JSON.stringify(searchIndex, null, 2)
|
||||
);
|
||||
|
||||
// Generate a manifest with counts and timestamps
|
||||
const manifest = {
|
||||
generated: new Date().toISOString(),
|
||||
counts: {
|
||||
agents: agents.length,
|
||||
prompts: prompts.length,
|
||||
instructions: instructions.length,
|
||||
skills: skills.length,
|
||||
collections: collections.length,
|
||||
total: searchIndex.length,
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "manifest.json"),
|
||||
JSON.stringify(manifest, null, 2)
|
||||
);
|
||||
|
||||
console.log(`\n✓ All data written to website/data/`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Error generating website data:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user