chore: publish from main

This commit is contained in:
github-actions[bot]
2026-07-16 04:10:37 +00:00
parent d86f40c898
commit d925e492ac
22 changed files with 64 additions and 1949 deletions
+1 -7
View File
@@ -6,7 +6,7 @@ A community-created collection of custom agents, instructions, skills, hooks, wo
> [!TIP] > [!TIP]
> **Explore the full collection on the website →** [awesome-copilot.github.com](https://awesome-copilot.github.com) > **Explore the full collection on the website →** [awesome-copilot.github.com](https://awesome-copilot.github.com)
> >
> The website offers full-text search and filtering across hundreds of resources, plus the [Tools](https://awesome-copilot.github.com/tools) section for MCP servers and developer tooling, and the [Learning Hub](https://awesome-copilot.github.com/learning-hub) for guides and tutorials. > The website offers full-text search and filtering across hundreds of resources, plus the [Learning Hub](https://awesome-copilot.github.com/learning-hub) for guides and tutorials.
> >
> **Using this collection in an AI agent?** A machine-readable [`llms.txt`](https://awesome-copilot.github.com/llms.txt) is available with structured listings of all agents, instructions, and skills. > **Using this collection in an AI agent?** A machine-readable [`llms.txt`](https://awesome-copilot.github.com/llms.txt) is available with structured listings of all agents, instructions, and skills.
@@ -22,14 +22,8 @@ New to GitHub Copilot customization? The **[Learning Hub](https://awesome-copilo
| 📋 [Instructions](docs/README.instructions.md) | Coding standards applied automatically by file pattern | [All instructions →](https://awesome-copilot.github.com/instructions) | | 📋 [Instructions](docs/README.instructions.md) | Coding standards applied automatically by file pattern | [All instructions →](https://awesome-copilot.github.com/instructions) |
| 🎯 [Skills](docs/README.skills.md) | Self-contained folders with instructions and bundled assets | [All skills →](https://awesome-copilot.github.com/skills) | | 🎯 [Skills](docs/README.skills.md) | Self-contained folders with instructions and bundled assets | [All skills →](https://awesome-copilot.github.com/skills) |
| 🔌 [Plugins](docs/README.plugins.md) | Curated bundles of agents and skills for specific workflows | [All plugins →](https://awesome-copilot.github.com/plugins) | | 🔌 [Plugins](docs/README.plugins.md) | Curated bundles of agents and skills for specific workflows | [All plugins →](https://awesome-copilot.github.com/plugins) |
| 🪝 [Hooks](docs/README.hooks.md) | Automated actions triggered during Copilot agent sessions | [All hooks →](https://awesome-copilot.github.com/hooks) |
| ⚡ [Agentic Workflows](docs/README.workflows.md) | AI-powered GitHub Actions automations written in markdown | [All workflows →](https://awesome-copilot.github.com/workflows) |
| 🍳 [Cookbook](cookbook/README.md) | Copy-paste-ready recipes for working with Copilot APIs | — | | 🍳 [Cookbook](cookbook/README.md) | Copy-paste-ready recipes for working with Copilot APIs | — |
## 🛠️ Tools
Looking at how to use Awesome Copilot? Check out the **[Tools section](https://awesome-copilot.github.com/tools)** of the website for MCP servers, editor integrations, and other developer tooling to get the most out of this collection.
## Install a Plugin ## Install a Plugin
For most users, the **Awesome Copilot** marketplace is already registered in the Copilot CLI/VS Code, so you can install a plugin directly: For most users, the **Awesome Copilot** marketplace is already registered in the Copilot CLI/VS Code, so you can install a plugin directly:
+32 -265
View File
@@ -2,7 +2,7 @@
/** /**
* Generate JSON metadata files for the GitHub Pages website. * Generate JSON metadata files for the GitHub Pages website.
* This script extracts metadata from agents, instructions, skills, hooks, and plugins * This script extracts metadata from agents, instructions, skills, and plugins
* and writes them to website/data/ for client-side search and display. * and writes them to website/data/ for client-side search and display.
*/ */
@@ -14,19 +14,15 @@ import {
AGENTS_DIR, AGENTS_DIR,
COOKBOOK_DIR, COOKBOOK_DIR,
EXTENSIONS_DIR, EXTENSIONS_DIR,
HOOKS_DIR,
INSTRUCTIONS_DIR, INSTRUCTIONS_DIR,
PLUGINS_DIR, PLUGINS_DIR,
ROOT_FOLDER, ROOT_FOLDER,
SKILLS_DIR, SKILLS_DIR,
WORKFLOWS_DIR,
} from "./constants.mjs"; } from "./constants.mjs";
import { getGitFileDates } from "./utils/git-dates.mjs"; import { getGitFileDates } from "./utils/git-dates.mjs";
import { import {
parseFrontmatter, parseFrontmatter,
parseHookMetadata,
parseSkillMetadata, parseSkillMetadata,
parseWorkflowMetadata,
parseYamlFile, parseYamlFile,
} from "./yaml-parser.mjs"; } from "./yaml-parser.mjs";
@@ -211,136 +207,6 @@ function generateAgentsData(gitDates) {
}; };
} }
/**
* Generate hooks metadata
*/
/**
* Generate hooks metadata (similar to skills - folder-based)
*/
function generateHooksData(gitDates) {
const hooks = [];
// Check if hooks directory exists
if (!fs.existsSync(HOOKS_DIR)) {
return {
items: hooks,
filters: {
hooks: [],
tags: [],
},
};
}
// Get all hook folders (directories)
const hookFolders = fs.readdirSync(HOOKS_DIR).filter((file) => {
const filePath = path.join(HOOKS_DIR, file);
return fs.statSync(filePath).isDirectory();
});
// Track all unique values for filters
const allHookTypes = new Set();
const allTags = new Set();
for (const folder of hookFolders) {
const hookPath = path.join(HOOKS_DIR, folder);
const metadata = parseHookMetadata(hookPath);
if (!metadata) continue;
const relativePath = path
.relative(ROOT_FOLDER, hookPath)
.replace(/\\/g, "/");
const readmeRelativePath = `${relativePath}/README.md`;
// Get all files in the hook folder recursively (for the file browser and
// ZIP download on the detail page).
const files = getFolderFiles(hookPath, relativePath);
// Track unique values
(metadata.hooks || []).forEach((h) => allHookTypes.add(h));
(metadata.tags || []).forEach((t) => allTags.add(t));
hooks.push({
id: folder,
title: metadata.name,
description: metadata.description,
hooks: metadata.hooks || [],
tags: metadata.tags || [],
assets: metadata.assets || [],
files,
path: relativePath,
readmeFile: readmeRelativePath,
readmeFileName: "README.md",
lastUpdated: gitDates.get(readmeRelativePath) || null,
});
}
// Sort and return with filter metadata
const sortedHooks = hooks.sort((a, b) => a.title.localeCompare(b.title));
return {
items: sortedHooks,
filters: {
hooks: Array.from(allHookTypes).sort(),
tags: Array.from(allTags).sort(),
},
};
}
/**
* Generate workflows metadata (flat .md files)
*/
function generateWorkflowsData(gitDates) {
const workflows = [];
if (!fs.existsSync(WORKFLOWS_DIR)) {
return {
items: workflows,
filters: {
triggers: [],
},
};
}
const workflowFiles = fs.readdirSync(WORKFLOWS_DIR).filter((file) => {
return file.endsWith(".md") && file !== ".gitkeep";
});
const allTriggers = new Set();
for (const file of workflowFiles) {
const filePath = path.join(WORKFLOWS_DIR, file);
const metadata = parseWorkflowMetadata(filePath);
if (!metadata) continue;
const relativePath = path
.relative(ROOT_FOLDER, filePath)
.replace(/\\/g, "/");
(metadata.triggers || []).forEach((t) => allTriggers.add(t));
const id = path.basename(file, ".md");
workflows.push({
id,
title: metadata.name,
description: metadata.description,
triggers: metadata.triggers || [],
path: relativePath,
lastUpdated: gitDates.get(relativePath) || null,
});
}
const sortedWorkflows = workflows.sort((a, b) =>
a.title.localeCompare(b.title)
);
return {
items: sortedWorkflows,
filters: {
triggers: Array.from(allTriggers).sort(),
},
};
}
/** /**
* Parse applyTo field into an array of patterns * Parse applyTo field into an array of patterns
*/ */
@@ -565,7 +431,7 @@ function getAgentFiles(agentDir, pluginRootPath) {
* Build a lookup index of resource id -> { title, url } for the kinds that have * Build a lookup index of resource id -> { title, url } for the kinds that have
* dedicated detail pages, so plugin items can deep-link to them. * dedicated detail pages, so plugin items can deep-link to them.
*/ */
function buildResourceIndex({ agents, skills, instructions, hooks, extensions }) { function buildResourceIndex({ agents, skills, instructions, extensions }) {
const toMap = (items, urlPrefix) => { const toMap = (items, urlPrefix) => {
const map = new Map(); const map = new Map();
for (const item of items || []) { for (const item of items || []) {
@@ -602,7 +468,6 @@ function buildResourceIndex({ agents, skills, instructions, hooks, extensions })
agent: toMap(agents, "agent"), agent: toMap(agents, "agent"),
skill: toMap(skills, "skill"), skill: toMap(skills, "skill"),
instruction: toMap(instructions, "instruction"), instruction: toMap(instructions, "instruction"),
hook: toMap(hooks, "hook"),
extension: extensionMap, extension: extensionMap,
}; };
} }
@@ -635,7 +500,7 @@ function resolvePluginItem(item, resourceIndex) {
return { return {
...item, ...item,
title: match?.title || candidateId || item.path, title: match?.title || item.title || candidateId || item.path,
detailUrl: match?.url || null, detailUrl: match?.url || null,
}; };
} }
@@ -749,12 +614,40 @@ function generatePluginsData(gitDates, resourceIndex = {}) {
]; ];
}); });
// Build items list from spec fields (agents, commands, skills) // Parse mcpServers: supports a path to a .mcp.json file or an inline object
const mcpItems = [];
if (data.mcpServers) {
let mcpServersObj = null;
let mcpConfigPath = relPath;
if (typeof data.mcpServers === "string") {
const manifestMcpPath = data.mcpServers.replace(/^\.\//, "");
mcpConfigPath = manifestMcpPath ? `${relPath}/${manifestMcpPath}` : relPath;
const mcpJsonPath = path.join(pluginDir, manifestMcpPath);
if (fs.existsSync(mcpJsonPath)) {
try {
const mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
mcpServersObj = mcpJson.mcpServers || mcpJson;
} catch {
// ignore parse errors
}
}
} else if (typeof data.mcpServers === "object") {
mcpServersObj = data.mcpServers;
}
if (mcpServersObj) {
for (const serverName of Object.keys(mcpServersObj)) {
mcpItems.push({ kind: "mcp", path: mcpConfigPath, title: serverName });
}
}
}
// Build items list from spec fields (agents, commands, skills, mcpServers)
const items = [ const items = [
...agentItems, ...agentItems,
...(data.commands || []).map((p) => ({ kind: "prompt", path: p })), ...(data.commands || []).map((p) => ({ kind: "prompt", path: p })),
...(data.skills || []).map((p) => ({ kind: "skill", path: p })), ...(data.skills || []).map((p) => ({ kind: "skill", path: p })),
...extensionItems, ...extensionItems,
...mcpItems,
].map((item) => resolvePluginItem(item, resourceIndex)); ].map((item) => resolvePluginItem(item, resourceIndex));
const tags = data.keywords || data.tags || []; const tags = data.keywords || data.tags || [];
@@ -1459,71 +1352,12 @@ function generateExtensionsData(extensionManifestData) {
return { items, filters }; return { items, filters };
} }
/**
* Generate tools metadata from website/data/tools.yml
*/
function generateToolsData() {
const toolsFile = path.join(WEBSITE_SOURCE_DATA_DIR, "tools.yml");
if (!fs.existsSync(toolsFile)) {
console.warn("No tools.yml file found at", toolsFile);
return { items: [], filters: { categories: [], tags: [] } };
}
const data = parseYamlFile(toolsFile);
if (!data || !data.tools) {
return { items: [], filters: { categories: [], tags: [] } };
}
const allCategories = new Set();
const allTags = new Set();
const tools = data.tools.map((tool) => {
const category = tool.category || "Other";
allCategories.add(category);
const tags = tool.tags || [];
tags.forEach((t) => allTags.add(t));
return {
id: tool.id,
name: tool.name,
description: tool.description || "",
category: category,
featured: tool.featured || false,
requirements: tool.requirements || [],
features: tool.features || [],
links: tool.links || {},
configuration: tool.configuration || null,
tags: tags,
};
});
// Sort with featured first, then alphabetically
const sortedTools = tools.sort((a, b) => {
if (a.featured && !b.featured) return -1;
if (!a.featured && b.featured) return 1;
return a.name.localeCompare(b.name);
});
return {
items: sortedTools,
filters: {
categories: Array.from(allCategories).sort(),
tags: Array.from(allTags).sort(),
},
};
}
/** /**
* Generate a combined index for search * Generate a combined index for search
*/ */
function generateSearchIndex( function generateSearchIndex(
agents, agents,
instructions, instructions,
hooks,
workflows,
skills, skills,
plugins plugins
) { ) {
@@ -1556,33 +1390,6 @@ function generateSearchIndex(
}); });
} }
for (const hook of hooks) {
index.push({
type: "hook",
id: hook.id,
title: hook.title,
description: hook.description,
path: hook.readmeFile,
lastUpdated: hook.lastUpdated,
searchText: `${hook.title} ${hook.description} ${hook.hooks.join(
" "
)} ${hook.tags.join(" ")}`.toLowerCase(),
});
}
for (const workflow of workflows) {
index.push({
type: "workflow",
id: workflow.id,
title: workflow.title,
description: workflow.description,
path: workflow.path,
lastUpdated: workflow.lastUpdated,
searchText: `${workflow.title} ${workflow.description
} ${workflow.triggers.join(" ")}`.toLowerCase(),
});
}
for (const skill of skills) { for (const skill of skills) {
index.push({ index.push({
type: "skill", type: "skill",
@@ -1752,8 +1559,6 @@ async function main() {
[ [
"agents/", "agents/",
"instructions/", "instructions/",
"hooks/",
"workflows/",
"skills/", "skills/",
"extensions/", "extensions/",
"plugins/", "plugins/",
@@ -1771,18 +1576,6 @@ async function main() {
`✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)` `✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)`
); );
const hooksData = generateHooksData(gitDates);
const hooks = hooksData.items;
console.log(
`✓ Generated ${hooks.length} hooks (${hooksData.filters.hooks.length} hook types, ${hooksData.filters.tags.length} tags)`
);
const workflowsData = generateWorkflowsData(gitDates);
const workflows = workflowsData.items;
console.log(
`✓ Generated ${workflows.length} workflows (${workflowsData.filters.triggers.length} triggers)`
);
const instructionsData = generateInstructionsData(gitDates); const instructionsData = generateInstructionsData(gitDates);
const instructions = instructionsData.items; const instructions = instructionsData.items;
console.log( console.log(
@@ -1804,7 +1597,6 @@ async function main() {
agents, agents,
skills, skills,
instructions, instructions,
hooks,
extensions, extensions,
}); });
const pluginsData = generatePluginsData(gitDates, resourceIndex); const pluginsData = generatePluginsData(gitDates, resourceIndex);
@@ -1813,12 +1605,6 @@ async function main() {
`✓ Generated ${plugins.length} plugins (${pluginsData.filters.tags.length} tags)` `✓ Generated ${plugins.length} plugins (${pluginsData.filters.tags.length} tags)`
); );
const toolsData = generateToolsData();
const tools = toolsData.items;
console.log(
`✓ Generated ${tools.length} tools (${toolsData.filters.categories.length} categories)`
);
const samplesData = generateSamplesData(); const samplesData = generateSamplesData();
console.log( console.log(
`✓ Generated ${samplesData.totalRecipes} recipes in ${samplesData.totalCookbooks} cookbooks (${samplesData.filters.languages.length} languages, ${samplesData.filters.tags.length} tags)` `✓ Generated ${samplesData.totalRecipes} recipes in ${samplesData.totalCookbooks} cookbooks (${samplesData.filters.languages.length} languages, ${samplesData.filters.tags.length} tags)`
@@ -1833,8 +1619,6 @@ async function main() {
const searchIndex = generateSearchIndex( const searchIndex = generateSearchIndex(
agents, agents,
instructions, instructions,
hooks,
workflows,
skills, skills,
plugins plugins
); );
@@ -1846,16 +1630,6 @@ async function main() {
JSON.stringify(agentsData, null, 2) JSON.stringify(agentsData, null, 2)
); );
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "hooks.json"),
JSON.stringify(hooksData, null, 2)
);
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "workflows.json"),
JSON.stringify(workflowsData, null, 2)
);
fs.writeFileSync( fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "instructions.json"), path.join(WEBSITE_DATA_DIR, "instructions.json"),
JSON.stringify(instructionsData, null, 2) JSON.stringify(instructionsData, null, 2)
@@ -1876,10 +1650,6 @@ async function main() {
JSON.stringify(extensionsData, null, 2) JSON.stringify(extensionsData, null, 2)
); );
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "tools.json"),
JSON.stringify(toolsData, null, 2)
);
fs.writeFileSync( fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "samples.json"), path.join(WEBSITE_DATA_DIR, "samples.json"),
@@ -1898,11 +1668,8 @@ async function main() {
agents: agents.length, agents: agents.length,
instructions: instructions.length, instructions: instructions.length,
skills: skills.length, skills: skills.length,
hooks: hooks.length,
workflows: workflows.length,
plugins: plugins.length, plugins: plugins.length,
extensions: extensions.length, extensions: extensions.length,
tools: tools.length,
contributors: contributorCount, contributors: contributorCount,
samples: samplesData.totalRecipes, samples: samplesData.totalRecipes,
total: searchIndex.length, total: searchIndex.length,
+2 -1
View File
@@ -21,5 +21,6 @@
"./skills/suggest-awesome-github-copilot-agents", "./skills/suggest-awesome-github-copilot-agents",
"./skills/suggest-awesome-github-copilot-instructions", "./skills/suggest-awesome-github-copilot-instructions",
"./skills/suggest-awesome-github-copilot-skills" "./skills/suggest-awesome-github-copilot-skills"
] ],
"mcpServers": "./.mcp.json"
} }
+14
View File
@@ -0,0 +1,14 @@
{
"mcpServers": {
"awesome-copilot": {
"type": "stdio",
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/microsoft/mcp-dotnet-samples/awesome-copilot:latest"
]
}
}
}
+9
View File
@@ -9,6 +9,11 @@ Meta prompts that help you discover and generate curated GitHub Copilot agents,
copilot plugin install awesome-copilot@awesome-copilot copilot plugin install awesome-copilot@awesome-copilot
``` ```
## Prerequisites
- [Docker](https://www.docker.com/) must be installed and available on your `PATH`.
- The plugin starts its bundled MCP server by running `docker run ... ghcr.io/microsoft/mcp-dotnet-samples/awesome-copilot:latest`.
## What's Included ## What's Included
### Commands (Slash Commands) ### Commands (Slash Commands)
@@ -26,6 +31,10 @@ copilot plugin install awesome-copilot@awesome-copilot
|-------|-------------| |-------|-------------|
| `meta-agentic-project-scaffold` | Meta agentic project creation assistant to help users create and manage project workflows effectively. | | `meta-agentic-project-scaffold` | Meta agentic project creation assistant to help users create and manage project workflows effectively. |
### MCP server
This plugin includes the `awesome-copilot` MCP server configured in [`./.mcp.json`](./.mcp.json). If Docker is unavailable, MCP startup will fail.
## Source ## Source
This plugin is part of [Awesome Copilot](https://github.com/github/awesome-copilot), a community-driven collection of GitHub Copilot extensions. This plugin is part of [Awesome Copilot](https://github.com/github/awesome-copilot), a community-driven collection of GitHub Copilot extensions.
-3
View File
@@ -129,11 +129,8 @@ export default defineConfig({
{ label: "Agents", link: "/agents/" }, { label: "Agents", link: "/agents/" },
{ label: "Instructions", link: "/instructions/" }, { label: "Instructions", link: "/instructions/" },
{ label: "Skills", link: "/skills/" }, { label: "Skills", link: "/skills/" },
{ label: "Hooks", link: "/hooks/" },
{ label: "Workflows", link: "/workflows/" },
{ label: "Canvas Extensions", link: "/extensions/" }, { label: "Canvas Extensions", link: "/extensions/" },
{ label: "Plugins", link: "/plugins/" }, { label: "Plugins", link: "/plugins/" },
{ label: "Tools", link: "/tools/" },
{ label: "Contributors", link: "/contributors/" }, { label: "Contributors", link: "/contributors/" },
], ],
}, },
@@ -21,9 +21,10 @@ const KIND_LABELS: Record<string, string> = {
hook: "Hooks", hook: "Hooks",
prompt: "Commands", prompt: "Commands",
extension: "Extensions", extension: "Extensions",
mcp: "MCP Servers",
}; };
const KIND_ORDER = ["agent", "skill", "instruction", "hook", "prompt", "extension"]; const KIND_ORDER = ["agent", "skill", "instruction", "hook", "prompt", "extension", "mcp"];
function githubHref(itemPath: string): string { function githubHref(itemPath: string): string {
const normalized = itemPath.replace(/^\.\/+/, "").replace(/\/+$/, ""); const normalized = itemPath.replace(/^\.\/+/, "").replace(/\/+$/, "");
@@ -77,7 +77,7 @@ Agentic Workflows are ideal when you need **autonomous, event-driven automation*
## Using Workflows from Awesome Copilot ## Using Workflows from Awesome Copilot
The [Awesome Copilot workflows page](../../workflows/) hosts a growing collection of community-contributed workflows. Here's how to install and use them. The [repository workflows directory](https://github.com/github/awesome-copilot/tree/main/workflows) hosts a growing collection of community-contributed workflows. Here's how to install and use them.
### Prerequisites ### Prerequisites
@@ -89,7 +89,7 @@ gh extension install github/gh-aw
### Installing a Workflow ### Installing a Workflow
1. **Browse** the [workflows collection](../../workflows/) and find one that fits your needs 1. **Browse** the [workflows collection](https://github.com/github/awesome-copilot/tree/main/workflows) and find one that fits your needs
2. **Copy** the workflow `.md` file into your repository's `.github/workflows/` directory 2. **Copy** the workflow `.md` file into your repository's `.github/workflows/` directory
3. **Compile** the workflow to generate the Actions lock file: 3. **Compile** the workflow to generate the Actions lock file:
@@ -211,8 +211,6 @@ gh aw compile --validate --no-emit workflows/my-new-workflow.md
## Learn More ## Learn More
- **Official documentation**: [GitHub Agentic Workflows](https://gh.io/gh-aw) — full specification and reference - **Official documentation**: [GitHub Agentic Workflows](https://gh.io/gh-aw) — full specification and reference
- **Browse workflows**: [Awesome Copilot Workflows](../../workflows/) — community-contributed collection
- **Contributing guide**: [CONTRIBUTING.md](https://github.com/github/awesome-copilot/blob/main/CONTRIBUTING.md#adding-agentic-workflows) — detailed contribution guidelines
- **Related**: [Automating with Hooks](../automating-with-hooks/) — deterministic automation for Copilot agent sessions - **Related**: [Automating with Hooks](../automating-with-hooks/) — deterministic automation for Copilot agent sessions
- **Related**: [Using the Copilot Coding Agent](../using-copilot-coding-agent/) — the agent that powers agentic workflows - **Related**: [Using the Copilot Coding Agent](../using-copilot-coding-agent/) — the agent that powers agentic workflows
@@ -681,7 +681,6 @@ A: Yes. Hooks are especially valuable with the coding agent because they provide
## Next Steps ## Next Steps
- **Explore Examples**: Browse the [Hooks Directory](../../hooks/) for ready-to-use hook configurations
- **Build Agents**: [Building Custom Agents](../building-custom-agents/) — Create agents that complement hooks - **Build Agents**: [Building Custom Agents](../building-custom-agents/) — Create agents that complement hooks
- **Automate Further**: [Using the Copilot Coding Agent](../using-copilot-coding-agent/) — Run hooks in autonomous agent sessions - **Automate Further**: [Using the Copilot Coding Agent](../using-copilot-coding-agent/) — Run hooks in autonomous agent sessions
@@ -327,7 +327,7 @@ This repository provides a curated collection of agents, skills, and hooks desig
### Adding Hooks from This Repo ### Adding Hooks from This Repo
1. Browse the [Hooks Directory](../../hooks/) for automation hooks 1. Browse the [Hooks documentation](https://github.com/github/awesome-copilot/blob/main/docs/README.hooks.md) for automation hooks
2. Copy the `hooks.json` content into a file in `.github/hooks/` in your repository 2. Copy the `hooks.json` content into a file in `.github/hooks/` in your repository
3. Copy any referenced scripts alongside it 3. Copy any referenced scripts alongside it
4. The hooks will run automatically during coding agent sessions 4. The hooks will run automatically during coding agent sessions
@@ -451,6 +451,6 @@ A: Yes. You can specify which agent to use when assigning work — the coding ag
- **Add Guardrails**: [Automating with Hooks](../automating-with-hooks/) — Ensure code quality in autonomous sessions - **Add Guardrails**: [Automating with Hooks](../automating-with-hooks/) — Ensure code quality in autonomous sessions
- **Build Custom Agents**: [Building Custom Agents](../building-custom-agents/) — Create specialized agents for the coding agent to use - **Build Custom Agents**: [Building Custom Agents](../building-custom-agents/) — Create specialized agents for the coding agent to use
- **Explore Configuration**: [Copilot Configuration Basics](../copilot-configuration-basics/) — Set up repository-level customizations - **Explore Configuration**: [Copilot Configuration Basics](../copilot-configuration-basics/) — Set up repository-level customizations
- **Browse Community Resources**: Explore the [Agents](../../agents/), [Skills](../../skills/), and [Hooks](../../hooks/) directories for ready-to-use resources - **Browse Community Resources**: Explore the [Agents](../../agents/), [Skills](../../skills/), and [Plugins](../../plugins/) directories for ready-to-use resources
--- ---
-159
View File
@@ -1,159 +0,0 @@
---
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
import BackToTop from "../../components/BackToTop.astro";
import hooksData from "../../../public/data/hooks.json";
import RawMarkdown from "../../components/pages/RawMarkdown.astro";
import type { HeaderItem } from "../../components/pages/Header.astro";
import DetailHeader from "../../components/pages/Header.astro";
import Breadcrumb from "../../components/pages/Breadcrumb.astro";
import Sidebar from "../../components/pages/Sidebar.astro";
import FileBrowser from "../../components/pages/FileBrowser.astro";
import { readResourceMarkdown, formatLastUpdated } from "../../lib/detail-page";
const GITHUB_BASE = "https://github.com/github/awesome-copilot/blob/main";
const GITHUB_TREE_BASE = "https://github.com/github/awesome-copilot/tree/main";
interface HookFile {
name: string;
path: string;
size?: number;
}
interface HookItem extends HeaderItem {
id: string;
title: string;
description?: string;
path: string;
readmeFile: string;
readmeFileName?: string;
files: HookFile[];
hooks: string[];
tags: string[];
assets: string[];
lastUpdated?: string | null;
}
export function getStaticPaths() {
return (hooksData.items as HookItem[]).map((item) => ({
params: { id: item.id },
props: { item },
}));
}
const { item } = Astro.props as { item: HookItem };
const { markdownHtml, frontmatterText, rawMarkdown } = readResourceMarkdown(
item.readmeFile
);
const lastUpdated = formatLastUpdated(item.lastUpdated);
const githubUrl = `${GITHUB_TREE_BASE}/${item.path}`;
const readmeFileName = item.readmeFileName ?? "README.md";
const fileCount = item.files.length;
const chips: {
title: string;
items: string[];
filterBase?: string;
filterParam?: string;
}[] = [];
if (item.hooks.length > 0) {
chips.push({ title: "Hook events", items: item.hooks });
}
chips.push({
title: "Files",
items: [`${fileCount} file${fileCount === 1 ? "" : "s"}`],
});
if (item.tags.length > 0) {
chips.push({
title: "Tags",
items: item.tags,
filterBase: "/hooks/",
filterParam: "tag",
});
}
---
<StarlightPage
frontmatter={{
title: item.title,
description: item.description,
template: "splash",
prev: false,
next: false,
editUrl: false,
}}
>
<div
id="main-content"
class="resource-detail-page hook-detail-page"
data-file-browser-page
data-bundle-id={item.id}
data-path={item.readmeFile}
>
<RawMarkdown markdown={rawMarkdown} />
<div class="container">
<Breadcrumb
paths={[
{ name: "Home", href: "/" },
{ name: "Hooks", href: "/hooks/" },
]}
title={item.title}
/>
<DetailHeader item={item} />
<div class="detail-layout">
<div class="detail-main">
<FileBrowser
files={item.files}
primaryPath={item.readmeFile}
primaryName={readmeFileName}
primaryHtml={markdownHtml}
githubBase={GITHUB_BASE}
/>
</div>
<Sidebar
item={item}
lastUpdated={lastUpdated ?? undefined}
frontmatterText={frontmatterText || undefined}
githubUrl={githubUrl}
chips={chips}
>
<div class="skill-install" slot="install">
<p class="skill-install-label">Install this hook</p>
<p class="skill-install-note">
Hooks are multi-file bundles with no CLI installer. Download the
ZIP and copy the files into your project's hooks directory.
</p>
<button
type="button"
class="btn btn-primary skill-download-btn"
data-action="download-zip"
>
<svg
viewBox="0 0 16 16"
width="16"
height="16"
fill="currentColor"
aria-hidden="true"
><path
d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"
></path><path
d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"
></path></svg
>
Download ZIP
</button>
</div>
</Sidebar>
</div>
</div>
</div>
<BackToTop />
<script>
import "../../scripts/pages/file-browser";
</script>
</StarlightPage>
-56
View File
@@ -1,56 +0,0 @@
---
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
import ContributeCTA from '../components/ContributeCTA.astro';
import PageHeader from '../components/PageHeader.astro';
import EmbeddedPageData from '../components/EmbeddedPageData.astro';
import BackToTop from '../components/BackToTop.astro';
import hooksData from '../../public/data/hooks.json';
import { renderHooksHtml, sortHooks } from '../scripts/pages/hooks-render';
const initialItems = sortHooks(hooksData.items, 'title');
---
<StarlightPage frontmatter={{ title: 'Hooks', description: 'Automated workflows triggered by Copilot coding agent events', template: 'splash', prev: false, next: false, editUrl: false }}>
<div id="main-content" class="hooks-page listing-cards-page">
<PageHeader title="Hooks" description="Automated workflows triggered by Copilot coding agent events" icon="hook" />
<div class="page-content">
<div class="container">
<div class="listing-toolbar">
<div class="listing-toolbar-row">
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} hooks</div>
<details class="listing-controls">
<summary class="listing-controls-trigger btn btn-secondary btn-small">Sort &amp; Filter</summary>
<div class="listing-controls-panel">
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label for="filter-tag">Tag:</label>
<select id="filter-tag" multiple aria-label="Filter by tag"></select>
</div>
<div class="filter-group">
<label for="sort-select">Sort:</label>
<select id="sort-select" aria-label="Sort by">
<option value="title">Name (A-Z)</option>
<option value="lastUpdated">Recently Updated</option>
</select>
</div>
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div>
</div>
</details>
</div>
</div>
<div class="resource-list" id="resource-list" role="list" set:html={renderHooksHtml(initialItems)}></div>
<ContributeCTA resourceType="hooks" />
</div>
</div>
</div>
<BackToTop />
<EmbeddedPageData filename="hooks.json" data={hooksData} />
<script>
import '../scripts/listing-flyouts';
import '../scripts/pages/hooks';
</script>
</StarlightPage>
-55
View File
@@ -116,44 +116,6 @@ const base = import.meta.env.BASE_URL;
- -
</div> </div>
</a> </a>
<a
href={`${base}hooks/`}
class="card card-with-count card-category-automation"
id="card-hooks"
style="--animation-delay: 150ms;"
>
<div class="card-icon" aria-hidden="true">
<Icon name="hook" size={40} />
</div>
<div class="card-content">
<h3>Hooks</h3>
<p>Automated workflows triggered by agent events</p>
</div>
<div class="card-count" data-count="hooks" aria-label="Hook count">
-
</div>
</a>
<a
href={`${base}workflows/`}
class="card card-with-count card-category-automation"
id="card-workflows"
style="--animation-delay: 200ms;"
>
<div class="card-icon" aria-hidden="true">
<Icon name="workflow" size={40} />
</div>
<div class="card-content">
<h3>Workflows</h3>
<p>AI-powered automations for GitHub Actions</p>
</div>
<div
class="card-count"
data-count="workflows"
aria-label="Workflow count"
>
-
</div>
</a>
<a <a
href={`${base}plugins/`} href={`${base}plugins/`}
class="card card-with-count card-category-extension" class="card card-with-count card-category-extension"
@@ -196,23 +158,6 @@ const base = import.meta.env.BASE_URL;
- -
</div> </div>
</a> </a>
<a
href={`${base}tools/`}
class="card card-with-count card-category-dev"
id="card-tools"
style="--animation-delay: 350ms;"
>
<div class="card-icon" aria-hidden="true">
<Icon name="wrench" size={40} />
</div>
<div class="card-content">
<h3>Tools</h3>
<p>MCP servers and developer tools</p>
</div>
<div class="card-count" data-count="tools" aria-label="Tool count">
-
</div>
</a>
<a <a
href={`${base}learning-hub/`} href={`${base}learning-hub/`}
class="card card-with-count card-category-learn" class="card card-with-count card-category-learn"
-277
View File
@@ -1,277 +0,0 @@
---
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
import toolsData from '../../public/data/tools.json';
import Modal from '../components/Modal.astro';
import ContributeCTA from "../components/ContributeCTA.astro";
import EmbeddedPageData from "../components/EmbeddedPageData.astro";
import PageHeader from "../components/PageHeader.astro";
import BackToTop from '../components/BackToTop.astro';
import { renderToolsHtml, sortTools } from "../scripts/pages/tools-render";
const initialItems = sortTools(
toolsData.items.map((item) => ({
...item,
title: item.name,
})),
"title"
);
---
<StarlightPage frontmatter={{ title: 'Tools', description: 'MCP servers and developer tools for GitHub Copilot', template: 'splash', prev: false, next: false, editUrl: false }}>
<div id="main-content">
<PageHeader title="Tools" description="MCP servers and developer tools for GitHub Copilot" icon="wrench" />
<div class="page-content">
<div class="container">
<div class="listing-toolbar">
<div class="listing-toolbar-row">
<div id="results-count" class="results-count" aria-live="polite">{initialItems.length} tools</div>
<details class="listing-controls">
<summary class="listing-controls-trigger btn btn-secondary btn-small">Sort &amp; Filter</summary>
<div class="listing-controls-panel">
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label for="filter-category">Category:</label>
<select id="filter-category" class="filter-select" aria-label="Filter by category">
<option value="">All Categories</option>
{toolsData.filters.categories.map((category) => (
<option value={category}>{category}</option>
))}
</select>
</div>
<div class="filter-group">
<label for="sort-select">Sort:</label>
<select id="sort-select" class="filter-select" aria-label="Sort tools">
<option value="featured">Featured First</option>
<option value="title">Title</option>
</select>
</div>
<button id="clear-filters" class="btn btn-secondary btn-small">Clear</button>
</div>
</div>
</details>
</div>
</div>
<div id="tools-list" set:html={renderToolsHtml(initialItems)}></div>
<div class="coming-soon">
<h2>More Tools Coming Soon</h2>
<p>
We're working on additional tools to enhance your GitHub Copilot
experience. Check back soon!
</p>
</div>
<ContributeCTA resourceType="tools" />
</div>
</div>
</div>
<Modal />
<BackToTop />
<EmbeddedPageData filename="tools.json" data={toolsData} />
<style is:global>
.empty-state {
text-align: center;
padding: 48px;
background-color: var(--color-bg-secondary);
border-radius: var(--border-radius-lg);
}
.empty-state h3 {
color: var(--color-text-muted);
margin-bottom: 8px;
}
.empty-state p {
color: var(--color-text-muted);
}
.tool-card {
background-color: var(--color-card-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-lg);
padding: 32px;
margin-bottom: 24px;
}
.tool-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.tool-header h2 {
margin: 0;
font-size: 24px;
color: var(--color-text-emphasis);
}
.tool-badges {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.tool-badge {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.tool-badge.featured {
background-color: var(--color-primary);
color: white;
}
.tool-badge.category {
background-color: var(--color-purple-light);
color: var(--color-purple-dark);
}
.tool-description {
color: var(--color-text-secondary);
font-size: 16px;
line-height: 1.6;
margin-bottom: 24px;
}
.tool-section {
margin-top: 24px;
}
.tool-section h3 {
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
color: var(--color-text-emphasis);
}
.tool-section ul {
margin: 0;
padding-left: 24px;
color: var(--color-text-muted);
}
.tool-section li {
margin-bottom: 8px;
}
.tool-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 20px;
}
.tool-tag {
background-color: var(--color-bg-secondary);
color: var(--color-text-muted);
padding: 4px 12px;
border-radius: 16px;
font-size: 12px;
border: 1px solid var(--color-border);
}
.tool-config {
margin-top: 24px;
}
.tool-config h3 {
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
color: var(--color-text-emphasis);
}
.tool-config-wrapper {
position: relative;
}
.tool-config pre {
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
padding: 16px;
overflow-x: auto;
margin: 0;
}
.tool-config code {
font-family: var(--font-mono);
font-size: 13px;
color: var(--color-text-primary);
white-space: pre;
}
.tool-actions {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid var(--color-border);
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.coming-soon {
text-align: center;
padding: 48px;
background-color: var(--color-bg-secondary);
border: 1px dashed var(--color-border);
border-radius: var(--border-radius-lg);
}
.coming-soon h2 {
color: var(--color-text-muted);
margin-bottom: 8px;
}
.coming-soon p {
color: var(--color-text-muted);
}
.copy-config-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
font-size: 13px;
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
color: var(--color-text-secondary);
cursor: pointer;
margin-top: 12px;
transition: all 0.2s;
}
.copy-config-btn:hover {
background-color: var(--color-bg-secondary);
color: var(--color-text-primary);
border-color: var(--color-text-muted);
}
.copy-config-btn.copied {
background-color: var(--color-success);
color: white;
border-color: var(--color-success);
}
/* Search highlight */
.search-highlight {
background-color: var(--color-warning);
color: var(--color-text-emphasis);
padding: 0 2px;
border-radius: 2px;
}
</style>
<script>
import '../scripts/listing-flyouts';
import '../scripts/pages/tools';
</script>
</StarlightPage>
-131
View File
@@ -1,131 +0,0 @@
---
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
import BackToTop from "../../components/BackToTop.astro";
import workflowsData from "../../../public/data/workflows.json";
import RawMarkdown from "../../components/pages/RawMarkdown.astro";
import type { HeaderItem } from "../../components/pages/Header.astro";
import DetailHeader from "../../components/pages/Header.astro";
import Breadcrumb from "../../components/pages/Breadcrumb.astro";
import Main from "../../components/pages/Main.astro";
import Sidebar from "../../components/pages/Sidebar.astro";
import { readResourceMarkdown, formatLastUpdated } from "../../lib/detail-page";
const GITHUB_BASE = "https://github.com/github/awesome-copilot/blob/main";
const workflowGuideHref = "/learning-hub/agentic-workflows/";
interface WorkflowItem extends HeaderItem {
id: string;
title: string;
description?: string;
path: string;
triggers: string[];
lastUpdated?: string | null;
}
export function getStaticPaths() {
return (workflowsData.items as WorkflowItem[]).map((item) => ({
params: { id: item.id },
props: { item },
}));
}
const { item } = Astro.props as { item: WorkflowItem };
const { markdownHtml, frontmatterText, rawMarkdown } = readResourceMarkdown(
item.path
);
const lastUpdated = formatLastUpdated(item.lastUpdated);
const githubUrl = `${GITHUB_BASE}/${item.path}`;
const chips: { title: string; items: string[] }[] = [];
if (item.triggers.length > 0) {
chips.push({ title: "Triggers", items: item.triggers });
}
---
<StarlightPage
frontmatter={{
title: item.title,
description: item.description,
template: "splash",
prev: false,
next: false,
editUrl: false,
}}
>
<div
id="main-content"
class="resource-detail-page"
data-resource-detail
data-path={item.path}
>
<RawMarkdown markdown={rawMarkdown} />
<div class="container">
<Breadcrumb
paths={[
{ name: "Home", href: "/" },
{ name: "Agentic Workflows", href: "/workflows/" },
]}
title={item.title}
/>
<DetailHeader item={item} />
<div class="detail-layout">
<Main markdownHtml={markdownHtml} githubUrl={githubUrl} />
<Sidebar
item={item}
lastUpdated={lastUpdated ?? undefined}
frontmatterText={frontmatterText || undefined}
githubUrl={githubUrl}
chips={chips}
>
<div class="skill-install" slot="install">
<p class="skill-install-label">Install this workflow</p>
<p class="skill-install-note">
Install the <code>gh aw</code> CLI{" "}
(<code>gh extension install github/gh-aw</code>), then copy this
file into <code>.github/workflows/</code> and run{" "}
<code>gh aw compile</code> to generate the lock file. See the{" "}
<a href={workflowGuideHref}>Agentic Workflows guide</a>{" "}
for full setup steps.
</p>
<button
type="button"
class="btn btn-primary skill-download-btn"
data-action="download"
>
<svg
viewBox="0 0 16 16"
width="16"
height="16"
fill="currentColor"
aria-hidden="true"
><path
d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"
></path><path
d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"
></path></svg
>
Download workflow
</button>
<button
type="button"
class="btn btn-secondary skill-download-btn"
data-action="copy-markdown"
>
Copy markdown
</button>
</div>
</Sidebar>
</div>
</div>
</div>
<BackToTop />
<script>
import "../../scripts/pages/resource-detail";
</script>
</StarlightPage>
-56
View File
@@ -1,56 +0,0 @@
---
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
import workflowsData from '../../public/data/workflows.json';
import ContributeCTA from '../components/ContributeCTA.astro';
import EmbeddedPageData from '../components/EmbeddedPageData.astro';
import PageHeader from '../components/PageHeader.astro';
import BackToTop from '../components/BackToTop.astro';
import { renderWorkflowsHtml, sortWorkflows } from '../scripts/pages/workflows-render';
const initialItems = sortWorkflows(workflowsData.items, 'title');
---
<StarlightPage frontmatter={{ title: 'Agentic Workflows', description: 'AI-powered repository automations that run coding agents in GitHub Actions', template: 'splash', prev: false, next: false, editUrl: false }}>
<div id="main-content" class="workflows-page listing-cards-page">
<PageHeader title="Agentic Workflows" description="AI-powered repository automations that run coding agents in GitHub Actions" icon="workflow" />
<div class="page-content">
<div class="container">
<div class="listing-toolbar">
<div class="listing-toolbar-row">
<div class="results-count" id="results-count" aria-live="polite">{initialItems.length} workflows</div>
<details class="listing-controls">
<summary class="listing-controls-trigger btn btn-secondary btn-small">Sort &amp; Filter</summary>
<div class="listing-controls-panel">
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<label for="filter-trigger">Trigger:</label>
<select id="filter-trigger" multiple aria-label="Filter by trigger"></select>
</div>
<div class="filter-group">
<label for="sort-select">Sort:</label>
<select id="sort-select" aria-label="Sort by">
<option value="title">Name (A-Z)</option>
<option value="lastUpdated">Recently Updated</option>
</select>
</div>
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div>
</div>
</details>
</div>
</div>
<div class="resource-list" id="resource-list" role="list" set:html={renderWorkflowsHtml(initialItems)}></div>
<ContributeCTA resourceType="workflows" />
</div>
</div>
</div>
<BackToTop />
<EmbeddedPageData filename="workflows.json" data={workflowsData} />
<script>
import '../scripts/listing-flyouts';
import '../scripts/pages/workflows';
</script>
</StarlightPage>
-104
View File
@@ -1,104 +0,0 @@
import {
escapeHtml,
getGitHubUrl,
getLastUpdatedHtml,
} from "../utils";
import { renderEmptyStateHtml, renderSharedCardHtml } from "./card-render";
export interface RenderableHookFile {
name: string;
path: string;
}
export interface RenderableHook {
id: string;
title: string;
description?: string;
path: string;
readmeFile: string;
hooks: string[];
tags: string[];
assets: string[];
files: RenderableHookFile[];
lastUpdated?: string | null;
}
export type HookSortOption = "title" | "lastUpdated";
/**
* Build the URL of a hook's dedicated detail page.
*/
export function getHookDetailUrl(id: string): string {
return `/hook/${id}/`;
}
export function sortHooks<T extends RenderableHook>(
items: T[],
sort: HookSortOption
): T[] {
return [...items].sort((a, b) => {
if (sort === "lastUpdated") {
const dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
const dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
return dateB - dateA;
}
return a.title.localeCompare(b.title);
});
}
export function renderHooksHtml(items: RenderableHook[]): string {
if (items.length === 0) {
return renderEmptyStateHtml("No hooks found", "Try adjusting the selected filters.");
}
return items
.map((item) => {
const metaHtml = `
${item.hooks
.map(
(hook) => `<span class="resource-tag tag-hook">${escapeHtml(hook)}</span>`
)
.join("")}
${item.tags
.map((tag) => `<span class="resource-tag tag-tag">${escapeHtml(tag)}</span>`)
.join("")}
${
item.assets.length > 0
? `<span class="resource-tag tag-assets">${item.assets.length} asset${
item.assets.length === 1 ? "" : "s"
}</span>`
: ""
}
${getLastUpdatedHtml(item.lastUpdated)}
`;
const actionsHtml = `
<button class="btn btn-primary download-hook-btn" data-hook-id="${escapeHtml(
item.id
)}" title="Download as ZIP">
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"/>
<path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"/>
</svg>
Download
</button>
<a href="${getGitHubUrl(
item.path
)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a>
`;
return renderSharedCardHtml({
title: item.title,
description: item.description || "No description",
href: getHookDetailUrl(item.id),
articleAttributes: {
"data-path": item.readmeFile,
"data-hook-id": item.id,
},
metaHtml,
actionsHtml,
});
})
.join("");
}
-199
View File
@@ -1,199 +0,0 @@
/**
* Hooks page functionality
*/
import {
fetchData,
getQueryParam,
getQueryParamValues,
showToast,
downloadZipBundle,
updateQueryParams,
} from '../utils';
import { clearSelectValues, getSelectValues, setSelectValues } from './select-utils';
import {
renderHooksHtml,
sortHooks,
type HookSortOption,
type RenderableHook,
} from './hooks-render';
interface Hook extends RenderableHook {}
interface HooksData {
items: Hook[];
filters: {
tags: string[];
};
}
let allItems: Hook[] = [];
let tagSelectEl: HTMLSelectElement | null = null;
let currentFilters = {
tags: [] as string[],
};
let currentSort: HookSortOption = 'title';
let resourceListHandlersReady = false;
function sortItems(items: Hook[]): Hook[] {
return sortHooks(items, currentSort);
}
function applyFiltersAndRender(): void {
const countEl = document.getElementById('results-count');
let results = [...allItems];
if (currentFilters.tags.length > 0) {
results = results.filter((item) => item.tags.some((tag) => currentFilters.tags.includes(tag)));
}
results = sortItems(results);
renderItems(results);
let countText = `${results.length} hook${results.length === 1 ? '' : 's'}`;
if (currentFilters.tags.length > 0) {
countText = `${results.length} of ${allItems.length} hooks (filtered by ${currentFilters.tags.length} tag${currentFilters.tags.length > 1 ? 's' : ''})`;
}
if (countEl) countEl.textContent = countText;
}
function renderItems(items: Hook[]): void {
const list = document.getElementById('resource-list');
if (!list) return;
list.innerHTML = renderHooksHtml(items);
}
async function downloadHook(hookId: string, btn: HTMLButtonElement): Promise<void> {
const hook = allItems.find((item) => item.id === hookId);
if (!hook) {
showToast('Hook not found.', 'error');
return;
}
const files = [
{ name: 'README.md', path: hook.readmeFile },
...hook.assets.map((asset) => ({
name: asset,
path: `${hook.path}/${asset}`,
})),
];
if (files.length === 0) {
showToast('No files found for this hook.', 'error');
return;
}
const originalContent = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';
try {
await downloadZipBundle(hook.id, files);
btn.innerHTML = '<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = originalContent;
}, 2000);
} catch (error) {
const message = error instanceof Error ? error.message : 'Download failed.';
showToast(message, 'error');
btn.innerHTML = '<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 0 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z"/></svg> Failed';
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = originalContent;
}, 2000);
}
}
function setupResourceListHandlers(list: HTMLElement | null): void {
if (!list || resourceListHandlersReady) return;
list.addEventListener('click', (event) => {
const target = event.target as HTMLElement;
const downloadButton = target.closest('.download-hook-btn') as HTMLButtonElement | null;
if (downloadButton) {
event.preventDefault();
event.stopPropagation();
const hookId = downloadButton.dataset.hookId;
if (hookId) downloadHook(hookId, downloadButton);
return;
}
});
resourceListHandlersReady = true;
}
function syncUrlState(): void {
updateQueryParams({
q: '',
hook: [],
tag: currentFilters.tags,
sort: currentSort === 'title' ? '' : currentSort,
});
}
export async function initHooksPage(): Promise<void> {
const list = document.getElementById('resource-list');
const clearFiltersBtn = document.getElementById('clear-filters');
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement | null;
setupResourceListHandlers(list as HTMLElement | null);
const data = await fetchData<HooksData>('hooks.json');
if (!data || !data.items) {
if (list) list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
return;
}
allItems = data.items;
tagSelectEl = document.getElementById('filter-tag') as HTMLSelectElement | null;
if (tagSelectEl) {
tagSelectEl.innerHTML = '';
data.filters.tags.forEach((tag) => {
const option = document.createElement('option');
option.value = tag;
option.textContent = tag;
tagSelectEl?.appendChild(option);
});
}
const initialTags = getQueryParamValues('tag').filter((tag) => data.filters.tags.includes(tag));
const initialSort = getQueryParam('sort');
if (initialTags.length > 0) {
currentFilters.tags = initialTags;
setSelectValues(tagSelectEl, initialTags);
}
if (initialSort === 'lastUpdated') {
currentSort = initialSort;
if (sortSelect) sortSelect.value = initialSort;
}
tagSelectEl?.addEventListener('change', () => {
currentFilters.tags = getSelectValues(tagSelectEl);
applyFiltersAndRender();
syncUrlState();
});
sortSelect?.addEventListener('change', () => {
currentSort = sortSelect.value as HookSortOption;
applyFiltersAndRender();
syncUrlState();
});
clearFiltersBtn?.addEventListener('click', () => {
currentFilters = { tags: [] };
currentSort = 'title';
clearSelectValues(tagSelectEl);
if (sortSelect) sortSelect.value = 'title';
applyFiltersAndRender();
syncUrlState();
});
applyFiltersAndRender();
}
// Auto-initialize when DOM is ready
document.addEventListener('DOMContentLoaded', initHooksPage);
-203
View File
@@ -1,203 +0,0 @@
import { escapeHtml } from "../utils";
export interface RenderableTool {
id: string;
name: string;
title: string;
description: string;
category: string;
featured: boolean;
requirements: string[];
features: string[];
links: {
blog?: string;
vscode?: string;
"vscode-insiders"?: string;
"visual-studio"?: string;
github?: string;
documentation?: string;
marketplace?: string;
npm?: string;
pypi?: string;
};
configuration?: {
type: string;
content: string;
} | null;
tags: string[];
}
export type ToolSortOption = "featured" | "title";
export function sortTools<T extends RenderableTool>(
tools: T[],
sort: ToolSortOption
): T[] {
return [...tools].sort((a, b) => {
if (sort === "featured") {
if (a.featured && !b.featured) return -1;
if (!a.featured && b.featured) return 1;
}
return a.title.localeCompare(b.title);
});
}
function formatMultilineText(text: string): string {
return escapeHtml(text).replace(/\r?\n/g, "<br>");
}
function sanitizeToolUrl(url: string): string {
try {
const protocol = new URL(url).protocol;
if (
protocol === "http:" ||
protocol === "https:" ||
protocol === "vscode:" ||
protocol === "vscode-insiders:"
) {
return escapeHtml(url);
}
} catch {
return "#";
}
return "#";
}
function getToolActionLink(
href: string | undefined,
label: string,
className: string
): string {
if (!href) return "";
return `<a href="${sanitizeToolUrl(
href
)}" class="${className}" target="_blank" rel="noopener">${label}</a>`;
}
export function renderToolsHtml(
tools: RenderableTool[]
): string {
if (tools.length === 0) {
return `
<div class="empty-state">
<h3>No tools found</h3>
<p>Try a different category or clear the current filters</p>
</div>
`;
}
return tools
.map((tool) => {
const badges: string[] = [];
if (tool.featured) {
badges.push('<span class="tool-badge featured">Featured</span>');
}
badges.push(
`<span class="tool-badge category">${escapeHtml(tool.category)}</span>`
);
const features =
tool.features && tool.features.length > 0
? `<div class="tool-section">
<h3>Features</h3>
<ul>${tool.features
.map((feature) => `<li>${escapeHtml(feature)}</li>`)
.join("")}</ul>
</div>`
: "";
const requirements =
tool.requirements && tool.requirements.length > 0
? `<div class="tool-section">
<h3>Requirements</h3>
<ul>${tool.requirements
.map((requirement) => `<li>${escapeHtml(requirement)}</li>`)
.join("")}</ul>
</div>`
: "";
const tags =
tool.tags && tool.tags.length > 0
? `<div class="tool-tags">
${tool.tags
.map((tag) => `<span class="tool-tag">${escapeHtml(tag)}</span>`)
.join("")}
</div>`
: "";
const config = tool.configuration
? `<div class="tool-config">
<h3>Configuration</h3>
<div class="tool-config-wrapper">
<pre><code>${escapeHtml(tool.configuration.content)}</code></pre>
</div>
<button class="copy-config-btn" data-config="${encodeURIComponent(
tool.configuration.content
)}">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/>
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/>
</svg>
Copy Configuration
</button>
</div>`
: "";
const actions = [
getToolActionLink(tool.links.blog, "📖 Blog", "btn btn-secondary"),
getToolActionLink(
tool.links.marketplace,
"🏪 Marketplace",
"btn btn-secondary"
),
getToolActionLink(tool.links.npm, "📦 npm", "btn btn-secondary"),
getToolActionLink(tool.links.pypi, "🐍 PyPI", "btn btn-secondary"),
getToolActionLink(
tool.links.documentation,
"📚 Docs",
"btn btn-secondary"
),
getToolActionLink(tool.links.github, "GitHub", "btn btn-secondary"),
getToolActionLink(
tool.links.vscode,
"Install in VS Code",
"btn btn-primary"
),
getToolActionLink(
tool.links["vscode-insiders"],
"VS Code Insiders",
"btn btn-outline"
),
getToolActionLink(
tool.links["visual-studio"],
"Visual Studio",
"btn btn-outline"
),
].filter(Boolean);
const actionsHtml =
actions.length > 0
? `<div class="tool-actions">${actions.join("")}</div>`
: "";
return `
<div class="tool-card">
<div class="tool-header">
<h2>${escapeHtml(tool.name)}</h2>
<div class="tool-badges">
${badges.join("")}
</div>
</div>
<p class="tool-description">${formatMultilineText(tool.description)}</p>
${features}
${requirements}
${config}
${tags}
${actionsHtml}
</div>
`;
})
.join("");
}
-211
View File
@@ -1,211 +0,0 @@
/**
* Tools page functionality
*/
import {
fetchData,
getQueryParam,
updateQueryParams,
} from "../utils";
import {
renderToolsHtml,
sortTools,
type ToolSortOption,
} from "./tools-render";
export interface Tool {
id: string;
name: string;
title: string;
description: string;
category: string;
featured: boolean;
requirements: string[];
features: string[];
links: {
blog?: string;
vscode?: string;
"vscode-insiders"?: string;
"visual-studio"?: string;
github?: string;
documentation?: string;
marketplace?: string;
npm?: string;
pypi?: string;
};
configuration?: {
type: string;
content: string;
};
tags: string[];
}
interface ToolsData {
items: Tool[];
filters: {
categories: string[];
tags: string[];
};
}
let allItems: Tool[] = [];
let currentFilters = {
categories: [] as string[],
};
let currentSort: ToolSortOption = "featured";
let copyHandlersReady = false;
let initialized = false;
function sortItems(items: Tool[]): Tool[] {
return sortTools(items, currentSort);
}
function getCountText(resultsCount: number): string {
if (currentFilters.categories.length === 0) {
return `${resultsCount} tool${resultsCount === 1 ? "" : "s"}`;
}
return `${resultsCount} of ${allItems.length} tools (filtered by ${currentFilters.categories.length} categor${currentFilters.categories.length === 1 ? "y" : "ies"})`;
}
function applyFiltersAndRender(): void {
const countEl = document.getElementById("results-count");
let results = [...allItems];
if (currentFilters.categories.length > 0) {
results = results.filter((item) =>
currentFilters.categories.includes(item.category)
);
}
results = sortItems(results);
renderTools(results);
if (countEl) countEl.textContent = getCountText(results.length);
}
function renderTools(tools: Tool[]): void {
const container = document.getElementById("tools-list");
if (!container) return;
container.innerHTML = renderToolsHtml(tools);
}
function syncUrlState(): void {
updateQueryParams({
q: "",
category: currentFilters.categories,
sort: currentSort === "featured" ? "" : currentSort,
});
}
function setupCopyConfigHandlers(): void {
if (copyHandlersReady) return;
document.addEventListener("click", async (event) => {
const button = (event.target as HTMLElement).closest(
".copy-config-btn"
) as HTMLButtonElement | null;
if (!button) return;
event.stopPropagation();
const config = decodeURIComponent(button.dataset.config || "");
try {
await navigator.clipboard.writeText(config);
button.classList.add("copied");
const originalHtml = button.innerHTML;
button.innerHTML = `
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/>
</svg>
Copied!
`;
setTimeout(() => {
button.classList.remove("copied");
button.innerHTML = originalHtml;
}, 2000);
} catch (err) {
console.error("Failed to copy:", err);
}
});
copyHandlersReady = true;
}
export async function initToolsPage(): Promise<void> {
if (initialized) return;
initialized = true;
const categoryFilter = document.getElementById(
"filter-category"
) as HTMLSelectElement;
const clearFiltersBtn = document.getElementById("clear-filters");
const sortSelect = document.getElementById("sort-select") as HTMLSelectElement;
const data = await fetchData<ToolsData>("tools.json");
if (!data || !data.items) {
const container = document.getElementById("tools-list");
if (container)
container.innerHTML =
'<div class="empty-state"><h3>Failed to load tools</h3></div>';
return;
}
// Map items to include title for FuzzySearch
allItems = data.items.map((item) => ({
...item,
title: item.name, // FuzzySearch uses title
}));
// Populate category filter
if (categoryFilter && data.filters.categories) {
categoryFilter.innerHTML =
'<option value="">All Categories</option>' +
data.filters.categories
.map(
(c) => `<option value="${c}">${c}</option>`
)
.join("");
const initialCategory = getQueryParam("category");
if (initialCategory && data.filters.categories.includes(initialCategory)) {
currentFilters.categories = [initialCategory];
categoryFilter.value = initialCategory;
}
categoryFilter.addEventListener("change", () => {
currentFilters.categories = categoryFilter.value
? [categoryFilter.value]
: [];
applyFiltersAndRender();
syncUrlState();
});
}
const initialSort = getQueryParam("sort");
if (initialSort === "title") {
currentSort = initialSort;
if (sortSelect) sortSelect.value = initialSort;
}
sortSelect?.addEventListener("change", () => {
currentSort = sortSelect.value as ToolSortOption;
applyFiltersAndRender();
syncUrlState();
});
applyFiltersAndRender();
syncUrlState();
// Clear filters
clearFiltersBtn?.addEventListener("click", () => {
currentFilters = { categories: [] };
currentSort = "featured";
if (categoryFilter) categoryFilter.value = "";
if (sortSelect) sortSelect.value = "featured";
applyFiltersAndRender();
syncUrlState();
});
setupCopyConfigHandlers();
}
// Auto-initialize when DOM is ready
document.addEventListener("DOMContentLoaded", initToolsPage);
@@ -1,75 +0,0 @@
import {
escapeHtml,
getActionButtonsHtml,
getGitHubUrl,
getLastUpdatedHtml,
} from '../utils';
import { renderEmptyStateHtml, renderSharedCardHtml } from './card-render';
export interface RenderableWorkflow {
id: string;
title: string;
description?: string;
path: string;
triggers: string[];
lastUpdated?: string | null;
}
export type WorkflowSortOption = 'title' | 'lastUpdated';
/**
* Build the URL of a workflow's dedicated detail page.
*/
export function getWorkflowDetailUrl(id: string): string {
return `/workflow/${id}/`;
}
export function sortWorkflows<T extends RenderableWorkflow>(
items: T[],
sort: WorkflowSortOption
): T[] {
return [...items].sort((a, b) => {
if (sort === 'lastUpdated') {
const dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
const dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
return dateB - dateA;
}
return a.title.localeCompare(b.title);
});
}
export function renderWorkflowsHtml(
items: RenderableWorkflow[]
): string {
if (items.length === 0) {
return renderEmptyStateHtml('No workflows found', 'Try adjusting the selected filters.');
}
return items
.map((item) => {
const metaHtml = `
${item.triggers
.map((trigger) => `<span class="resource-tag tag-trigger">${escapeHtml(trigger)}</span>`)
.join('')}
${getLastUpdatedHtml(item.lastUpdated)}
`;
const actionsHtml = `
${getActionButtonsHtml(item.path)}
<a href="${getGitHubUrl(item.path)}" class="btn btn-secondary" target="_blank" onclick="event.stopPropagation()" title="View on GitHub">GitHub</a>
`;
return renderSharedCardHtml({
title: item.title,
description: item.description || 'No description',
href: getWorkflowDetailUrl(item.id),
articleAttributes: {
'data-path': item.path,
},
metaHtml,
actionsHtml,
});
})
.join('');
}
-139
View File
@@ -1,139 +0,0 @@
/**
* Workflows page functionality
*/
import {
fetchData,
getQueryParam,
getQueryParamValues,
setupActionHandlers,
updateQueryParams,
} from '../utils';
import { clearSelectValues, getSelectValues, setSelectValues } from './select-utils';
import {
renderWorkflowsHtml,
sortWorkflows,
type RenderableWorkflow,
type WorkflowSortOption,
} from './workflows-render';
interface Workflow extends RenderableWorkflow {
id: string;
path: string;
triggers: string[];
lastUpdated?: string | null;
}
interface WorkflowsData {
items: Workflow[];
filters: {
triggers: string[];
};
}
let allItems: Workflow[] = [];
let triggerSelectEl: HTMLSelectElement | null = null;
let currentFilters = {
triggers: [] as string[],
};
let currentSort: WorkflowSortOption = 'title';
function sortItems(items: Workflow[]): Workflow[] {
return sortWorkflows(items, currentSort);
}
function applyFiltersAndRender(): void {
const countEl = document.getElementById('results-count');
let results = [...allItems];
if (currentFilters.triggers.length > 0) {
results = results.filter((item) => item.triggers.some((trigger) => currentFilters.triggers.includes(trigger)));
}
results = sortItems(results);
renderItems(results);
let countText = `${results.length} workflow${results.length === 1 ? '' : 's'}`;
if (currentFilters.triggers.length > 0) {
countText = `${results.length} of ${allItems.length} workflows (filtered by ${currentFilters.triggers.length} trigger${currentFilters.triggers.length > 1 ? 's' : ''})`;
}
if (countEl) countEl.textContent = countText;
}
function renderItems(items: Workflow[]): void {
const list = document.getElementById('resource-list');
if (!list) return;
list.innerHTML = renderWorkflowsHtml(items);
}
function syncUrlState(): void {
updateQueryParams({
q: '',
trigger: currentFilters.triggers,
sort: currentSort === 'title' ? '' : currentSort,
});
}
export async function initWorkflowsPage(): Promise<void> {
const list = document.getElementById('resource-list');
const clearFiltersBtn = document.getElementById('clear-filters');
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement | null;
const data = await fetchData<WorkflowsData>('workflows.json');
if (!data || !data.items) {
if (list) list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
return;
}
allItems = data.items;
triggerSelectEl = document.getElementById('filter-trigger') as HTMLSelectElement | null;
if (triggerSelectEl) {
triggerSelectEl.innerHTML = '';
data.filters.triggers.forEach((trigger) => {
const option = document.createElement('option');
option.value = trigger;
option.textContent = trigger;
triggerSelectEl?.appendChild(option);
});
}
const initialTriggers = getQueryParamValues('trigger').filter((trigger) => data.filters.triggers.includes(trigger));
const initialSort = getQueryParam('sort');
if (initialTriggers.length > 0) {
currentFilters.triggers = initialTriggers;
setSelectValues(triggerSelectEl, initialTriggers);
}
if (initialSort === 'lastUpdated') {
currentSort = initialSort;
if (sortSelect) sortSelect.value = initialSort;
}
triggerSelectEl?.addEventListener('change', () => {
currentFilters.triggers = getSelectValues(triggerSelectEl);
applyFiltersAndRender();
syncUrlState();
});
sortSelect?.addEventListener('change', () => {
currentSort = sortSelect.value as WorkflowSortOption;
applyFiltersAndRender();
syncUrlState();
});
clearFiltersBtn?.addEventListener('click', () => {
currentFilters = { triggers: [] };
currentSort = 'title';
clearSelectValues(triggerSelectEl);
if (sortSelect) sortSelect.value = 'title';
applyFiltersAndRender();
syncUrlState();
});
applyFiltersAndRender();
setupActionHandlers();
}
// Auto-initialize when DOM is ready
document.addEventListener('DOMContentLoaded', initWorkflowsPage);