mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-17 19:31:20 +00:00
chore: publish from main
This commit is contained in:
@@ -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
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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 & 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>
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 & 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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 & 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>
|
|
||||||
@@ -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("");
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -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("");
|
|
||||||
}
|
|
||||||
@@ -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('');
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
Reference in New Issue
Block a user