mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-23 11:55:12 +00:00
Merge pull request #496 from github/digitarald/exotic-raven
New awesome agent primitive
This commit is contained in:
16
.github/copilot-instructions.md
vendored
16
.github/copilot-instructions.md
vendored
@@ -39,3 +39,19 @@ The following instructions are only to be applied when performing a code review.
|
|||||||
- [ ] The file name is lower case, with words separated by hyphens.
|
- [ ] The file name is lower case, with words separated by hyphens.
|
||||||
- [ ] Encourage the use of `tools`, but it's not required.
|
- [ ] Encourage the use of `tools`, but it's not required.
|
||||||
- [ ] Strongly encourage the use of `model` to specify the model that the chat mode is optimised for.
|
- [ ] Strongly encourage the use of `model` to specify the model that the chat mode is optimised for.
|
||||||
|
|
||||||
|
## Agent Skills guide
|
||||||
|
|
||||||
|
**Only apply to folders in the `skills/` directory**
|
||||||
|
|
||||||
|
- [ ] The skill folder contains a `SKILL.md` file.
|
||||||
|
- [ ] The SKILL.md has markdown front matter.
|
||||||
|
- [ ] The SKILL.md has a `name` field.
|
||||||
|
- [ ] The `name` field value is lowercase with words separated by hyphens.
|
||||||
|
- [ ] The `name` field matches the folder name.
|
||||||
|
- [ ] The SKILL.md has a `description` field.
|
||||||
|
- [ ] The `description` field is not empty, at least 10 characters, and maximum 1024 characters.
|
||||||
|
- [ ] The `description` field value is wrapped in single quotes.
|
||||||
|
- [ ] The folder name is lower case, with words separated by hyphens.
|
||||||
|
- [ ] Any bundled assets (scripts, templates, data files) are referenced in the SKILL.md instructions.
|
||||||
|
- [ ] Bundled assets are reasonably sized (under 5MB per file).
|
||||||
|
|||||||
@@ -50,13 +50,13 @@
|
|||||||
"path": {
|
"path": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Relative path from repository root to the item file",
|
"description": "Relative path from repository root to the item file",
|
||||||
"pattern": "^(prompts|instructions|agents)/[^/]+\\.(prompt|instructions|agent)\\.md$",
|
"pattern": "^(?:skills/[^/]+/SKILL\\.md|(prompts|instructions|agents)/[^/]+\\.(prompt|instructions|agent)\\.md)$",
|
||||||
"minLength": 1
|
"minLength": 1
|
||||||
},
|
},
|
||||||
"kind": {
|
"kind": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Type of the item",
|
"description": "Type of the item",
|
||||||
"enum": ["prompt", "instruction", "agent"]
|
"enum": ["prompt", "instruction", "agent", "skill"]
|
||||||
},
|
},
|
||||||
"usage": {
|
"usage": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
38
.vscode/tasks.json
vendored
38
.vscode/tasks.json
vendored
@@ -41,9 +41,33 @@
|
|||||||
"${input:tags}"
|
"${input:tags}"
|
||||||
],
|
],
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"group": "build",
|
"group": "none",
|
||||||
"detail": "Creates a new collection manifest template.",
|
"detail": "Creates a new collection manifest template.",
|
||||||
"dependsOn": "npm install"
|
"dependsOn": "npm install"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "create-skill",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "npm run skill:create",
|
||||||
|
"args": [
|
||||||
|
"--name",
|
||||||
|
"${input:skillName}",
|
||||||
|
"--description",
|
||||||
|
"${input:skillDescription}"
|
||||||
|
],
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": "none",
|
||||||
|
"detail": "Creates a new skill template.",
|
||||||
|
"dependsOn": "npm install"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "validate-skills",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "npm run skill:validate",
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": "build",
|
||||||
|
"detail": "Validates all skill manifest files.",
|
||||||
|
"dependsOn": "npm install"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
@@ -58,6 +82,18 @@
|
|||||||
"description": "Comma separated list of tags",
|
"description": "Comma separated list of tags",
|
||||||
"default": "tag1,tag2",
|
"default": "tag1,tag2",
|
||||||
"type": "promptString"
|
"type": "promptString"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "skillName",
|
||||||
|
"description": "Skill name (PascalCase)",
|
||||||
|
"default": "MySkill",
|
||||||
|
"type": "promptString"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "skillDescription",
|
||||||
|
"description": "Brief description of the skill",
|
||||||
|
"default": "A brief description of my skill.",
|
||||||
|
"type": "promptString"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
43
AGENTS.md
43
AGENTS.md
@@ -7,6 +7,7 @@ The Awesome GitHub Copilot repository is a community-driven collection of custom
|
|||||||
- **Agents** - Specialized GitHub Copilot agents that integrate with MCP servers
|
- **Agents** - Specialized GitHub Copilot agents that integrate with MCP servers
|
||||||
- **Prompts** - Task-specific prompts for code generation and problem-solving
|
- **Prompts** - Task-specific prompts for code generation and problem-solving
|
||||||
- **Instructions** - Coding standards and best practices applied to specific file patterns
|
- **Instructions** - Coding standards and best practices applied to specific file patterns
|
||||||
|
- **Skills** - Self-contained folders with instructions and bundled resources for specialized tasks
|
||||||
- **Collections** - Curated collections organized around specific themes and workflows
|
- **Collections** - Curated collections organized around specific themes and workflows
|
||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
@@ -16,6 +17,7 @@ The Awesome GitHub Copilot repository is a community-driven collection of custom
|
|||||||
├── agents/ # Custom GitHub Copilot agent definitions (.agent.md files)
|
├── agents/ # Custom GitHub Copilot agent definitions (.agent.md files)
|
||||||
├── prompts/ # Task-specific prompts (.prompt.md files)
|
├── prompts/ # Task-specific prompts (.prompt.md files)
|
||||||
├── instructions/ # Coding standards and guidelines (.instructions.md files)
|
├── instructions/ # Coding standards and guidelines (.instructions.md files)
|
||||||
|
├── skills/ # Agent Skills folders (each with SKILL.md and optional bundled assets)
|
||||||
├── collections/ # Curated collections of resources (.md files)
|
├── collections/ # Curated collections of resources (.md files)
|
||||||
├── docs/ # Documentation for different resource types
|
├── docs/ # Documentation for different resource types
|
||||||
├── eng/ # Build and automation scripts
|
├── eng/ # Build and automation scripts
|
||||||
@@ -36,13 +38,19 @@ npm run collection:validate
|
|||||||
|
|
||||||
# Create a new collection
|
# Create a new collection
|
||||||
npm run collection:create -- --id <collection-id> --tags <tags>
|
npm run collection:create -- --id <collection-id> --tags <tags>
|
||||||
|
|
||||||
|
# Validate agent skills
|
||||||
|
npm run skill:validate
|
||||||
|
|
||||||
|
# Create a new skill
|
||||||
|
npm run skill:create -- --name <skill-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
### Working with Agents, Prompts, and Instructions
|
### Working with Agents, Prompts, Instructions, and Skills
|
||||||
|
|
||||||
All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction files (`*.instructions.md`) must include proper markdown front matter:
|
All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction files (`*.instructions.md`) must include proper markdown front matter. Agent Skills are folders containing a `SKILL.md` file with frontmatter and optional bundled assets:
|
||||||
|
|
||||||
#### Agent Files (*.agent.md)
|
#### Agent Files (*.agent.md)
|
||||||
- Must have `description` field (wrapped in single quotes)
|
- Must have `description` field (wrapped in single quotes)
|
||||||
@@ -62,20 +70,40 @@ All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction fi
|
|||||||
- Must have `applyTo` field specifying file patterns (e.g., `'**.js, **.ts'`)
|
- Must have `applyTo` field specifying file patterns (e.g., `'**.js, **.ts'`)
|
||||||
- File names should be lower case with words separated by hyphens
|
- File names should be lower case with words separated by hyphens
|
||||||
|
|
||||||
|
#### Agent Skills (skills/*/SKILL.md)
|
||||||
|
- Each skill is a folder containing a `SKILL.md` file
|
||||||
|
- SKILL.md must have `name` field (lowercase with hyphens, matching folder name, max 64 characters)
|
||||||
|
- SKILL.md must have `description` field (wrapped in single quotes, 10-1024 characters)
|
||||||
|
- Folder names should be lower case with words separated by hyphens
|
||||||
|
- Skills can include bundled assets (scripts, templates, data files)
|
||||||
|
- Bundled assets should be referenced in the SKILL.md instructions
|
||||||
|
- Asset files should be reasonably sized (under 5MB per file)
|
||||||
|
- Skills follow the [Agent Skills specification](https://agentskills.io/specification)
|
||||||
|
|
||||||
### Adding New Resources
|
### Adding New Resources
|
||||||
|
|
||||||
When adding a new agent, prompt, or instruction file:
|
When adding a new agent, prompt, instruction, or skill:
|
||||||
|
|
||||||
|
**For Agents, Prompts, and Instructions:**
|
||||||
1. Create the file with proper front matter
|
1. Create the file with proper front matter
|
||||||
2. Add the file to the appropriate directory
|
2. Add the file to the appropriate directory
|
||||||
3. Update the README.md by running: `npm run build`
|
3. Update the README.md by running: `npm run build`
|
||||||
4. Verify the resource appears in the generated README
|
4. Verify the resource appears in the generated README
|
||||||
|
|
||||||
|
**For Skills:**
|
||||||
|
1. Run `npm run skill:create` to scaffold a new skill folder
|
||||||
|
2. Edit the generated SKILL.md file with your instructions
|
||||||
|
3. Add any bundled assets (scripts, templates, data) to the skill folder
|
||||||
|
4. Run `npm run skill:validate` to validate the skill structure
|
||||||
|
5. Update the README.md by running: `npm run build`
|
||||||
|
6. Verify the skill appears in the generated README
|
||||||
|
|
||||||
### Testing Instructions
|
### Testing Instructions
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all validation checks
|
# Run all validation checks
|
||||||
npm run collection:validate
|
npm run collection:validate
|
||||||
|
npm run skill:validate
|
||||||
|
|
||||||
# Build and verify README generation
|
# Build and verify README generation
|
||||||
npm run build
|
npm run build
|
||||||
@@ -148,6 +176,15 @@ For agent files (*.agent.md):
|
|||||||
- [ ] Includes `model` field (strongly recommended)
|
- [ ] Includes `model` field (strongly recommended)
|
||||||
- [ ] Considers using `tools` field
|
- [ ] Considers using `tools` field
|
||||||
|
|
||||||
|
For skills (skills/*/):
|
||||||
|
- [ ] Folder contains a SKILL.md file
|
||||||
|
- [ ] SKILL.md has markdown front matter
|
||||||
|
- [ ] Has `name` field matching folder name (lowercase with hyphens, max 64 characters)
|
||||||
|
- [ ] Has non-empty `description` field wrapped in single quotes (10-1024 characters)
|
||||||
|
- [ ] Folder name is lower case with hyphens
|
||||||
|
- [ ] Any bundled assets are referenced in SKILL.md
|
||||||
|
- [ ] Bundled assets are under 5MB per file
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
This is a community-driven project. Contributions are welcome! Please see:
|
This is a community-driven project. Contributions are welcome! Please see:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ This repository provides a comprehensive toolkit for enhancing GitHub Copilot wi
|
|||||||
- **👉 [Awesome Agents](docs/README.agents.md)** - Specialized GitHub Copilot agents that integrate with MCP servers to provide enhanced capabilities for specific workflows and tools
|
- **👉 [Awesome Agents](docs/README.agents.md)** - Specialized GitHub Copilot agents that integrate with MCP servers to provide enhanced capabilities for specific workflows and tools
|
||||||
- **👉 [Awesome Prompts](docs/README.prompts.md)** - Focused, task-specific prompts for generating code, documentation, and solving specific problems
|
- **👉 [Awesome Prompts](docs/README.prompts.md)** - Focused, task-specific prompts for generating code, documentation, and solving specific problems
|
||||||
- **👉 [Awesome Instructions](docs/README.instructions.md)** - Comprehensive coding standards and best practices that apply to specific file patterns or entire projects
|
- **👉 [Awesome Instructions](docs/README.instructions.md)** - Comprehensive coding standards and best practices that apply to specific file patterns or entire projects
|
||||||
|
- **👉 [Awesome Skills](docs/README.skills.md)** - Self-contained folders with instructions and bundled resources that enhance AI capabilities for specialized tasks
|
||||||
- **👉 [Awesome Collections](docs/README.collections.md)** - Curated collections of related prompts, instructions, and chat modes organized around specific themes and workflows
|
- **👉 [Awesome Collections](docs/README.collections.md)** - Curated collections of related prompts, instructions, and chat modes organized around specific themes and workflows
|
||||||
|
|
||||||
## 🌟 Featured Collections
|
## 🌟 Featured Collections
|
||||||
|
|||||||
25
docs/README.skills.md
Normal file
25
docs/README.skills.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# 🎯 Agent Skills
|
||||||
|
|
||||||
|
Agent Skills are self-contained folders with instructions and bundled resources that enhance AI capabilities for specialized tasks. Based on the [Agent Skills specification](https://agentskills.io/specification), each skill contains a `SKILL.md` file with detailed instructions that agents load on-demand.
|
||||||
|
|
||||||
|
Skills differ from other primitives by supporting bundled assets (scripts, code samples, reference data) that agents can utilize when performing specialized tasks.
|
||||||
|
### How to Use Agent Skills
|
||||||
|
|
||||||
|
**What's Included:**
|
||||||
|
- Each skill is a folder containing a `SKILL.md` instruction file
|
||||||
|
- Skills may include helper scripts, code templates, or reference data
|
||||||
|
- Skills follow the Agent Skills specification for maximum compatibility
|
||||||
|
|
||||||
|
**When to Use:**
|
||||||
|
- Skills are ideal for complex, repeatable workflows that benefit from bundled resources
|
||||||
|
- Use skills when you need code templates, helper utilities, or reference data alongside instructions
|
||||||
|
- Skills provide progressive disclosure - loaded only when needed for specific tasks
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
- Browse the skills table below to find relevant capabilities
|
||||||
|
- Copy the skill folder to your local skills directory
|
||||||
|
- Reference skills in your prompts or let the agent discover them automatically
|
||||||
|
|
||||||
|
| Name | Description | Bundled Assets |
|
||||||
|
| ---- | ----------- | -------------- |
|
||||||
|
| [webapp-testing](../skills/webapp-testing/SKILL.md) | Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs. | `test-helper.js` |
|
||||||
@@ -77,6 +77,29 @@ Custom agents for GitHub Copilot, making it easy for users and organizations to
|
|||||||
- Access installed agents through the VS Code Chat interface, assign them in CCA, or through Copilot CLI (coming soon)
|
- Access installed agents through the VS Code Chat interface, assign them in CCA, or through Copilot CLI (coming soon)
|
||||||
- Agents will have access to tools from configured MCP servers
|
- Agents will have access to tools from configured MCP servers
|
||||||
- Follow agent-specific instructions for optimal usage`,
|
- Follow agent-specific instructions for optimal usage`,
|
||||||
|
|
||||||
|
skillsSection: `## 🎯 Agent Skills
|
||||||
|
|
||||||
|
Agent Skills are self-contained folders with instructions and bundled resources that enhance AI capabilities for specialized tasks. Based on the [Agent Skills specification](https://agentskills.io/specification), each skill contains a \`SKILL.md\` file with detailed instructions that agents load on-demand.
|
||||||
|
|
||||||
|
Skills differ from other primitives by supporting bundled assets (scripts, code samples, reference data) that agents can utilize when performing specialized tasks.`,
|
||||||
|
|
||||||
|
skillsUsage: `### How to Use Agent Skills
|
||||||
|
|
||||||
|
**What's Included:**
|
||||||
|
- Each skill is a folder containing a \`SKILL.md\` instruction file
|
||||||
|
- Skills may include helper scripts, code templates, or reference data
|
||||||
|
- Skills follow the Agent Skills specification for maximum compatibility
|
||||||
|
|
||||||
|
**When to Use:**
|
||||||
|
- Skills are ideal for complex, repeatable workflows that benefit from bundled resources
|
||||||
|
- Use skills when you need code templates, helper utilities, or reference data alongside instructions
|
||||||
|
- Skills provide progressive disclosure - loaded only when needed for specific tasks
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
- Browse the skills table below to find relevant capabilities
|
||||||
|
- Copy the skill folder to your local skills directory
|
||||||
|
- Reference skills in your prompts or let the agent discover them automatically`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const vscodeInstallImage =
|
const vscodeInstallImage =
|
||||||
@@ -98,9 +121,16 @@ const ROOT_FOLDER = path.join(__dirname, "..");
|
|||||||
const INSTRUCTIONS_DIR = path.join(ROOT_FOLDER, "instructions");
|
const INSTRUCTIONS_DIR = path.join(ROOT_FOLDER, "instructions");
|
||||||
const PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts");
|
const PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts");
|
||||||
const AGENTS_DIR = path.join(ROOT_FOLDER, "agents");
|
const AGENTS_DIR = path.join(ROOT_FOLDER, "agents");
|
||||||
|
const SKILLS_DIR = path.join(ROOT_FOLDER, "skills");
|
||||||
const COLLECTIONS_DIR = path.join(ROOT_FOLDER, "collections");
|
const COLLECTIONS_DIR = path.join(ROOT_FOLDER, "collections");
|
||||||
const MAX_COLLECTION_ITEMS = 50;
|
const MAX_COLLECTION_ITEMS = 50;
|
||||||
|
|
||||||
|
// Agent Skills validation constants
|
||||||
|
const SKILL_NAME_MIN_LENGTH = 1;
|
||||||
|
const SKILL_NAME_MAX_LENGTH = 64;
|
||||||
|
const SKILL_DESCRIPTION_MIN_LENGTH = 10;
|
||||||
|
const SKILL_DESCRIPTION_MAX_LENGTH = 1024;
|
||||||
|
|
||||||
const DOCS_DIR = path.join(ROOT_FOLDER, "docs");
|
const DOCS_DIR = path.join(ROOT_FOLDER, "docs");
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -113,8 +143,13 @@ export {
|
|||||||
INSTRUCTIONS_DIR,
|
INSTRUCTIONS_DIR,
|
||||||
PROMPTS_DIR,
|
PROMPTS_DIR,
|
||||||
AGENTS_DIR,
|
AGENTS_DIR,
|
||||||
|
SKILLS_DIR,
|
||||||
COLLECTIONS_DIR,
|
COLLECTIONS_DIR,
|
||||||
MAX_COLLECTION_ITEMS,
|
MAX_COLLECTION_ITEMS,
|
||||||
|
SKILL_NAME_MIN_LENGTH,
|
||||||
|
SKILL_NAME_MAX_LENGTH,
|
||||||
|
SKILL_DESCRIPTION_MIN_LENGTH,
|
||||||
|
SKILL_DESCRIPTION_MAX_LENGTH,
|
||||||
DOCS_DIR,
|
DOCS_DIR,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
219
eng/create-skill.mjs
Normal file
219
eng/create-skill.mjs
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import readline from "readline";
|
||||||
|
import { SKILLS_DIR } from "./constants.mjs";
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
function prompt(question) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(question, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const out = { name: undefined, description: undefined };
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = args[i];
|
||||||
|
if (a === "--name" || a === "-n") {
|
||||||
|
out.name = args[i + 1];
|
||||||
|
i++;
|
||||||
|
} else if (a.startsWith("--name=")) {
|
||||||
|
out.name = a.split("=")[1];
|
||||||
|
} else if (a === "--description" || a === "-d") {
|
||||||
|
out.description = args[i + 1];
|
||||||
|
i++;
|
||||||
|
} else if (a.startsWith("--description=")) {
|
||||||
|
out.description = a.split("=")[1];
|
||||||
|
} else if (!a.startsWith("-") && !out.name) {
|
||||||
|
out.name = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSkillTemplate() {
|
||||||
|
try {
|
||||||
|
console.log("🎯 Agent Skills Creator");
|
||||||
|
console.log(
|
||||||
|
"This tool will help you create a new skill following the Agent Skills specification.\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsed = parseArgs();
|
||||||
|
|
||||||
|
// Get skill name
|
||||||
|
let skillName = parsed.name;
|
||||||
|
if (!skillName) {
|
||||||
|
skillName = await prompt("Skill name (lowercase, hyphens only): ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate skill name format
|
||||||
|
if (!skillName) {
|
||||||
|
console.error("❌ Skill name is required");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[a-z0-9-]+$/.test(skillName)) {
|
||||||
|
console.error(
|
||||||
|
"❌ Skill name must contain only lowercase letters, numbers, and hyphens"
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillFolder = path.join(SKILLS_DIR, skillName);
|
||||||
|
|
||||||
|
// Check if folder already exists
|
||||||
|
if (fs.existsSync(skillFolder)) {
|
||||||
|
console.log(`⚠️ Skill folder ${skillName} already exists at ${skillFolder}`);
|
||||||
|
console.log("💡 Please choose a different name or edit the existing skill.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get description
|
||||||
|
let description = parsed.description;
|
||||||
|
if (!description) {
|
||||||
|
description = await prompt(
|
||||||
|
"Description (what this skill does and when to use it): "
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!description || description.trim().length < 10) {
|
||||||
|
console.error(
|
||||||
|
"❌ Description is required and must be at least 10 characters (max 1024)"
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get skill title (display name)
|
||||||
|
const defaultTitle = skillName
|
||||||
|
.split("-")
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
let skillTitle = await prompt(`Skill title (default: ${defaultTitle}): `);
|
||||||
|
if (!skillTitle.trim()) {
|
||||||
|
skillTitle = defaultTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create skill folder
|
||||||
|
fs.mkdirSync(skillFolder, { recursive: true });
|
||||||
|
|
||||||
|
// Create SKILL.md template
|
||||||
|
const skillMdContent = `---
|
||||||
|
name: ${skillName}
|
||||||
|
description: ${description}
|
||||||
|
---
|
||||||
|
|
||||||
|
# ${skillTitle}
|
||||||
|
|
||||||
|
This skill provides [brief overview of what this skill does].
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Use this skill when you need to:
|
||||||
|
- [Primary use case]
|
||||||
|
- [Secondary use case]
|
||||||
|
- [Additional use case]
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [Required tool/environment]
|
||||||
|
- [Optional dependency]
|
||||||
|
|
||||||
|
## Core Capabilities
|
||||||
|
|
||||||
|
### 1. [Capability Name]
|
||||||
|
[Description of what this capability does]
|
||||||
|
|
||||||
|
### 2. [Capability Name]
|
||||||
|
[Description of what this capability does]
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Example 1: [Use Case]
|
||||||
|
\`\`\`[language]
|
||||||
|
// Example code or instructions
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Example 2: [Use Case]
|
||||||
|
\`\`\`[language]
|
||||||
|
// Example code or instructions
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
1. **[Guideline 1]** - [Explanation]
|
||||||
|
2. **[Guideline 2]** - [Explanation]
|
||||||
|
3. **[Guideline 3]** - [Explanation]
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern: [Pattern Name]
|
||||||
|
\`\`\`[language]
|
||||||
|
// Example pattern
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Pattern: [Pattern Name]
|
||||||
|
\`\`\`[language]
|
||||||
|
// Example pattern
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- [Limitation 1]
|
||||||
|
- [Limitation 2]
|
||||||
|
- [Limitation 3]
|
||||||
|
`;
|
||||||
|
|
||||||
|
const skillFilePath = path.join(skillFolder, "SKILL.md");
|
||||||
|
fs.writeFileSync(skillFilePath, skillMdContent);
|
||||||
|
|
||||||
|
console.log(`\n✅ Created skill folder: ${skillFolder}`);
|
||||||
|
console.log(`✅ Created SKILL.md: ${skillFilePath}`);
|
||||||
|
|
||||||
|
// Ask if they want to add bundled assets
|
||||||
|
const addAssets = await prompt(
|
||||||
|
"\nWould you like to add bundled assets? (helper scripts, templates, etc.) [y/N]: "
|
||||||
|
);
|
||||||
|
|
||||||
|
if (addAssets.toLowerCase() === "y" || addAssets.toLowerCase() === "yes") {
|
||||||
|
console.log(
|
||||||
|
"\n📁 You can now add files to the skill folder manually or using your editor."
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
" Common bundled assets: helper scripts, code templates, reference data"
|
||||||
|
);
|
||||||
|
console.log(` Skill folder location: ${skillFolder}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n📝 Next steps:");
|
||||||
|
console.log("1. Edit SKILL.md to complete the skill instructions");
|
||||||
|
console.log("2. Add any bundled assets (scripts, templates, data) to the skill folder");
|
||||||
|
console.log("3. Run 'npm run skill:validate' to validate the skill");
|
||||||
|
console.log("4. Run 'npm run build' to generate documentation");
|
||||||
|
|
||||||
|
console.log("\n📖 Resources:");
|
||||||
|
console.log(
|
||||||
|
" - Anthropic Skills Spec: https://agentskills.io/specification"
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
" - Project Documentation: AGENTS.md (section on Agent Skills)"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error creating skill template: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the interactive creation process
|
||||||
|
createSkillTemplate();
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path, { dirname } from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { dirname } from "path";
|
|
||||||
import {
|
import {
|
||||||
parseCollectionYaml,
|
|
||||||
extractMcpServers,
|
|
||||||
extractMcpServerConfigs,
|
|
||||||
parseFrontmatter,
|
|
||||||
} from "./yaml-parser.mjs";
|
|
||||||
import {
|
|
||||||
TEMPLATES,
|
|
||||||
AKA_INSTALL_URLS,
|
|
||||||
repoBaseUrl,
|
|
||||||
vscodeInstallImage,
|
|
||||||
vscodeInsidersInstallImage,
|
|
||||||
ROOT_FOLDER,
|
|
||||||
PROMPTS_DIR,
|
|
||||||
AGENTS_DIR,
|
AGENTS_DIR,
|
||||||
|
AKA_INSTALL_URLS,
|
||||||
COLLECTIONS_DIR,
|
COLLECTIONS_DIR,
|
||||||
INSTRUCTIONS_DIR,
|
|
||||||
DOCS_DIR,
|
DOCS_DIR,
|
||||||
|
INSTRUCTIONS_DIR,
|
||||||
|
PROMPTS_DIR,
|
||||||
|
repoBaseUrl,
|
||||||
|
ROOT_FOLDER,
|
||||||
|
SKILLS_DIR,
|
||||||
|
TEMPLATES,
|
||||||
|
vscodeInsidersInstallImage,
|
||||||
|
vscodeInstallImage,
|
||||||
} from "./constants.mjs";
|
} from "./constants.mjs";
|
||||||
|
import {
|
||||||
|
extractMcpServerConfigs,
|
||||||
|
parseCollectionYaml,
|
||||||
|
parseFrontmatter,
|
||||||
|
parseSkillMetadata,
|
||||||
|
} from "./yaml-parser.mjs";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -51,34 +51,37 @@ let MCP_REGISTRY_SET = null;
|
|||||||
*/
|
*/
|
||||||
async function loadMcpRegistryNames() {
|
async function loadMcpRegistryNames() {
|
||||||
if (MCP_REGISTRY_SET) return MCP_REGISTRY_SET;
|
if (MCP_REGISTRY_SET) return MCP_REGISTRY_SET;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Fetching MCP registry from API...');
|
console.log("Fetching MCP registry from API...");
|
||||||
const allServers = [];
|
const allServers = [];
|
||||||
let cursor = null;
|
let cursor = null;
|
||||||
const apiUrl = 'https://api.mcp.github.com/v0.1/servers/';
|
const apiUrl = "https://api.mcp.github.com/v0.1/servers/";
|
||||||
|
|
||||||
// Fetch all pages using cursor-based pagination
|
// Fetch all pages using cursor-based pagination
|
||||||
do {
|
do {
|
||||||
const url = cursor ? `${apiUrl}?cursor=${encodeURIComponent(cursor)}` : apiUrl;
|
const url = cursor
|
||||||
|
? `${apiUrl}?cursor=${encodeURIComponent(cursor)}`
|
||||||
|
: apiUrl;
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`API returned status ${response.status}`);
|
throw new Error(`API returned status ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
const servers = json?.servers || [];
|
const servers = json?.servers || [];
|
||||||
|
|
||||||
// Extract server names and displayNames from the response
|
// Extract server names and displayNames from the response
|
||||||
for (const entry of servers) {
|
for (const entry of servers) {
|
||||||
const serverName = entry?.server?.name;
|
const serverName = entry?.server?.name;
|
||||||
if (serverName) {
|
if (serverName) {
|
||||||
// Try to get displayName from GitHub metadata, fall back to server name
|
// Try to get displayName from GitHub metadata, fall back to server name
|
||||||
const displayName =
|
const displayName =
|
||||||
entry?.server?._meta?.["io.modelcontextprotocol.registry/publisher-provided"]?.github?.displayName ||
|
entry?.server?._meta?.[
|
||||||
serverName;
|
"io.modelcontextprotocol.registry/publisher-provided"
|
||||||
|
]?.github?.displayName || serverName;
|
||||||
|
|
||||||
allServers.push({
|
allServers.push({
|
||||||
name: serverName,
|
name: serverName,
|
||||||
displayName: displayName.toLowerCase(),
|
displayName: displayName.toLowerCase(),
|
||||||
@@ -87,18 +90,18 @@ async function loadMcpRegistryNames() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get next cursor for pagination
|
// Get next cursor for pagination
|
||||||
cursor = json?.metadata?.nextCursor || null;
|
cursor = json?.metadata?.nextCursor || null;
|
||||||
} while (cursor);
|
} while (cursor);
|
||||||
|
|
||||||
console.log(`Loaded ${allServers.length} servers from MCP registry`);
|
console.log(`Loaded ${allServers.length} servers from MCP registry`);
|
||||||
MCP_REGISTRY_SET = allServers;
|
MCP_REGISTRY_SET = allServers;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Failed to load MCP registry from API: ${e.message}`);
|
console.warn(`Failed to load MCP registry from API: ${e.message}`);
|
||||||
MCP_REGISTRY_SET = [];
|
MCP_REGISTRY_SET = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return MCP_REGISTRY_SET;
|
return MCP_REGISTRY_SET;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,28 +432,31 @@ function generateMcpServerLinks(servers, registryNames) {
|
|||||||
|
|
||||||
// Match against both displayName and full name (case-insensitive)
|
// Match against both displayName and full name (case-insensitive)
|
||||||
const serverNameLower = serverName.toLowerCase();
|
const serverNameLower = serverName.toLowerCase();
|
||||||
const registryEntry = registryNames.find(
|
const registryEntry = registryNames.find((entry) => {
|
||||||
(entry) => {
|
// Exact match on displayName or fullName
|
||||||
// Exact match on displayName or fullName
|
if (
|
||||||
if (entry.displayName === serverNameLower || entry.fullName === serverNameLower) {
|
entry.displayName === serverNameLower ||
|
||||||
|
entry.fullName === serverNameLower
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the serverName matches a part of the full name after a slash
|
||||||
|
// e.g., "apify" matches "com.apify/apify-mcp-server"
|
||||||
|
const nameParts = entry.fullName.split("/");
|
||||||
|
if (nameParts.length > 1 && nameParts[1]) {
|
||||||
|
// Check if it matches the second part (after the slash)
|
||||||
|
const secondPart = nameParts[1]
|
||||||
|
.replace("-mcp-server", "")
|
||||||
|
.replace("-mcp", "");
|
||||||
|
if (secondPart === serverNameLower) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the serverName matches a part of the full name after a slash
|
|
||||||
// e.g., "apify" matches "com.apify/apify-mcp-server"
|
|
||||||
const nameParts = entry.fullName.split('/');
|
|
||||||
if (nameParts.length > 1 && nameParts[1]) {
|
|
||||||
// Check if it matches the second part (after the slash)
|
|
||||||
const secondPart = nameParts[1].replace('-mcp-server', '').replace('-mcp', '');
|
|
||||||
if (secondPart === serverNameLower) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if serverName matches the displayName ignoring case
|
|
||||||
return entry.displayName === serverNameLower;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
// Check if serverName matches the displayName ignoring case
|
||||||
|
return entry.displayName === serverNameLower;
|
||||||
|
});
|
||||||
const serverLabel = registryEntry
|
const serverLabel = registryEntry
|
||||||
? `[${serverName}](${`https://github.com/mcp/${registryEntry.name}`})`
|
? `[${serverName}](${`https://github.com/mcp/${registryEntry.name}`})`
|
||||||
: serverName;
|
: serverName;
|
||||||
@@ -477,6 +483,62 @@ function generateAgentsSection(agentsDir, registryNames = []) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the skills section with a table of all skills
|
||||||
|
*/
|
||||||
|
function generateSkillsSection(skillsDir) {
|
||||||
|
if (!fs.existsSync(skillsDir)) {
|
||||||
|
console.log(`Skills directory does not exist: ${skillsDir}`);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all skill folders (directories)
|
||||||
|
const skillFolders = fs.readdirSync(skillsDir).filter((file) => {
|
||||||
|
const filePath = path.join(skillsDir, file);
|
||||||
|
return fs.statSync(filePath).isDirectory();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse each skill folder
|
||||||
|
const skillEntries = skillFolders
|
||||||
|
.map((folder) => {
|
||||||
|
const skillPath = path.join(skillsDir, folder);
|
||||||
|
const metadata = parseSkillMetadata(skillPath);
|
||||||
|
if (!metadata) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
folder,
|
||||||
|
name: metadata.name,
|
||||||
|
description: metadata.description,
|
||||||
|
assets: metadata.assets,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((entry) => entry !== null)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
console.log(`Found ${skillEntries.length} skill(s)`);
|
||||||
|
|
||||||
|
if (skillEntries.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create table header
|
||||||
|
let content =
|
||||||
|
"| Name | Description | Bundled Assets |\n| ---- | ----------- | -------------- |\n";
|
||||||
|
|
||||||
|
// Generate table rows for each skill
|
||||||
|
for (const skill of skillEntries) {
|
||||||
|
const link = `../skills/${skill.folder}/SKILL.md`;
|
||||||
|
const assetsList =
|
||||||
|
skill.assets.length > 0
|
||||||
|
? skill.assets.map((a) => `\`${a}\``).join("<br />")
|
||||||
|
: "None";
|
||||||
|
|
||||||
|
content += `| [${skill.name}](${link}) | ${skill.description} | ${assetsList} |\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${TEMPLATES.skillsSection}\n${TEMPLATES.skillsUsage}\n\n${content}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unified generator for chat modes & agents (future consolidation)
|
* Unified generator for chat modes & agents (future consolidation)
|
||||||
* @param {Object} cfg
|
* @param {Object} cfg
|
||||||
@@ -708,7 +770,11 @@ function generateFeaturedCollectionsSection(collectionsDir) {
|
|||||||
* @param {string} collectionId - Collection ID
|
* @param {string} collectionId - Collection ID
|
||||||
* @param {{ name: string, displayName: string }[]} registryNames - Pre-loaded MCP registry names
|
* @param {{ name: string, displayName: string }[]} registryNames - Pre-loaded MCP registry names
|
||||||
*/
|
*/
|
||||||
function generateCollectionReadme(collection, collectionId, registryNames = []) {
|
function generateCollectionReadme(
|
||||||
|
collection,
|
||||||
|
collectionId,
|
||||||
|
registryNames = []
|
||||||
|
) {
|
||||||
if (!collection || !collection.items) {
|
if (!collection || !collection.items) {
|
||||||
return `# ${collectionId}\n\nCollection not found or invalid.`;
|
return `# ${collectionId}\n\nCollection not found or invalid.`;
|
||||||
}
|
}
|
||||||
@@ -760,20 +826,23 @@ function generateCollectionReadme(collection, collectionId, registryNames = [])
|
|||||||
? "Instruction"
|
? "Instruction"
|
||||||
: item.kind === "agent"
|
: item.kind === "agent"
|
||||||
? "Agent"
|
? "Agent"
|
||||||
|
: item.kind === "skill"
|
||||||
|
? "Skill"
|
||||||
: "Prompt";
|
: "Prompt";
|
||||||
const link = `../${item.path}`;
|
const link = `../${item.path}`;
|
||||||
|
|
||||||
// Create install badges for each item
|
// Create install badges for each item (skills don't use chat install badges)
|
||||||
const badges = makeBadges(
|
const badgeType =
|
||||||
item.path,
|
|
||||||
item.kind === "instruction"
|
item.kind === "instruction"
|
||||||
? "instructions"
|
? "instructions"
|
||||||
: item.kind === "chat-mode"
|
: item.kind === "chat-mode"
|
||||||
? "mode"
|
? "mode"
|
||||||
: item.kind === "agent"
|
: item.kind === "agent"
|
||||||
? "agent"
|
? "agent"
|
||||||
: "prompt"
|
: item.kind === "skill"
|
||||||
);
|
? null
|
||||||
|
: "prompt";
|
||||||
|
const badges = badgeType ? makeBadges(item.path, badgeType) : "";
|
||||||
|
|
||||||
const usageDescription = item.usage
|
const usageDescription = item.usage
|
||||||
? `${description} [see usage](#${title
|
? `${description} [see usage](#${title
|
||||||
@@ -831,15 +900,21 @@ function buildCollectionRow({
|
|||||||
kind,
|
kind,
|
||||||
registryNames = [],
|
registryNames = [],
|
||||||
}) {
|
}) {
|
||||||
|
const titleCell = badges
|
||||||
|
? `[${title}](${link})<br />${badges}`
|
||||||
|
: `[${title}](${link})`;
|
||||||
|
|
||||||
if (hasAgents) {
|
if (hasAgents) {
|
||||||
// Only agents currently have MCP servers; future migration may extend to chat modes.
|
// Only agents currently have MCP servers; future migration may extend to chat modes.
|
||||||
const mcpServers =
|
const mcpServers =
|
||||||
kind === "agent" ? extractMcpServerConfigs(filePath) : [];
|
kind === "agent" ? extractMcpServerConfigs(filePath) : [];
|
||||||
const mcpServerCell =
|
const mcpServerCell =
|
||||||
mcpServers.length > 0 ? generateMcpServerLinks(mcpServers, registryNames) : "";
|
mcpServers.length > 0
|
||||||
return `| [${title}](${link})<br />${badges} | ${typeDisplay} | ${usageDescription} | ${mcpServerCell} |\n`;
|
? generateMcpServerLinks(mcpServers, registryNames)
|
||||||
|
: "";
|
||||||
|
return `| ${titleCell} | ${typeDisplay} | ${usageDescription} | ${mcpServerCell} |\n`;
|
||||||
}
|
}
|
||||||
return `| [${title}](${link})<br />${badges} | ${typeDisplay} | ${usageDescription} |\n`;
|
return `| ${titleCell} | ${typeDisplay} | ${usageDescription} |\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility: write file only if content changed
|
// Utility: write file only if content changed
|
||||||
@@ -861,7 +936,13 @@ function writeFileIfChanged(filePath, content) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build per-category README content using existing generators, upgrading headings to H1
|
// Build per-category README content using existing generators, upgrading headings to H1
|
||||||
function buildCategoryReadme(sectionBuilder, dirPath, headerLine, usageLine, registryNames = []) {
|
function buildCategoryReadme(
|
||||||
|
sectionBuilder,
|
||||||
|
dirPath,
|
||||||
|
headerLine,
|
||||||
|
usageLine,
|
||||||
|
registryNames = []
|
||||||
|
) {
|
||||||
const section = sectionBuilder(dirPath, registryNames);
|
const section = sectionBuilder(dirPath, registryNames);
|
||||||
if (section && section.trim()) {
|
if (section && section.trim()) {
|
||||||
// Upgrade the first markdown heading level from ## to # for standalone README files
|
// Upgrade the first markdown heading level from ## to # for standalone README files
|
||||||
@@ -886,6 +967,7 @@ async function main() {
|
|||||||
);
|
);
|
||||||
const promptsHeader = TEMPLATES.promptsSection.replace(/^##\s/m, "# ");
|
const promptsHeader = TEMPLATES.promptsSection.replace(/^##\s/m, "# ");
|
||||||
const agentsHeader = TEMPLATES.agentsSection.replace(/^##\s/m, "# ");
|
const agentsHeader = TEMPLATES.agentsSection.replace(/^##\s/m, "# ");
|
||||||
|
const skillsHeader = TEMPLATES.skillsSection.replace(/^##\s/m, "# ");
|
||||||
const collectionsHeader = TEMPLATES.collectionsSection.replace(
|
const collectionsHeader = TEMPLATES.collectionsSection.replace(
|
||||||
/^##\s/m,
|
/^##\s/m,
|
||||||
"# "
|
"# "
|
||||||
@@ -914,103 +996,115 @@ async function main() {
|
|||||||
registryNames
|
registryNames
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate collections README
|
// Generate skills README
|
||||||
const collectionsReadme = buildCategoryReadme(
|
const skillsReadme = buildCategoryReadme(
|
||||||
generateCollectionsSection,
|
generateSkillsSection,
|
||||||
COLLECTIONS_DIR,
|
SKILLS_DIR,
|
||||||
collectionsHeader,
|
skillsHeader,
|
||||||
TEMPLATES.collectionsUsage,
|
TEMPLATES.skillsUsage,
|
||||||
registryNames
|
registryNames
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ensure docs directory exists for category outputs
|
// Generate collections README
|
||||||
if (!fs.existsSync(DOCS_DIR)) {
|
const collectionsReadme = buildCategoryReadme(
|
||||||
fs.mkdirSync(DOCS_DIR, { recursive: true });
|
generateCollectionsSection,
|
||||||
}
|
COLLECTIONS_DIR,
|
||||||
|
collectionsHeader,
|
||||||
|
TEMPLATES.collectionsUsage,
|
||||||
|
registryNames
|
||||||
|
);
|
||||||
|
|
||||||
// Write category outputs into docs folder
|
// Ensure docs directory exists for category outputs
|
||||||
writeFileIfChanged(
|
if (!fs.existsSync(DOCS_DIR)) {
|
||||||
path.join(DOCS_DIR, "README.instructions.md"),
|
fs.mkdirSync(DOCS_DIR, { recursive: true });
|
||||||
instructionsReadme
|
}
|
||||||
);
|
|
||||||
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
|
|
||||||
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
|
|
||||||
writeFileIfChanged(
|
|
||||||
path.join(DOCS_DIR, "README.collections.md"),
|
|
||||||
collectionsReadme
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate individual collection README files
|
// Write category outputs into docs folder
|
||||||
if (fs.existsSync(COLLECTIONS_DIR)) {
|
writeFileIfChanged(
|
||||||
console.log("Generating individual collection README files...");
|
path.join(DOCS_DIR, "README.instructions.md"),
|
||||||
|
instructionsReadme
|
||||||
|
);
|
||||||
|
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
|
||||||
|
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
|
||||||
|
writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme);
|
||||||
|
writeFileIfChanged(
|
||||||
|
path.join(DOCS_DIR, "README.collections.md"),
|
||||||
|
collectionsReadme
|
||||||
|
);
|
||||||
|
|
||||||
const collectionFiles = fs
|
// Generate individual collection README files
|
||||||
.readdirSync(COLLECTIONS_DIR)
|
if (fs.existsSync(COLLECTIONS_DIR)) {
|
||||||
.filter((file) => file.endsWith(".collection.yml"));
|
console.log("Generating individual collection README files...");
|
||||||
|
|
||||||
for (const file of collectionFiles) {
|
const collectionFiles = fs
|
||||||
const filePath = path.join(COLLECTIONS_DIR, file);
|
.readdirSync(COLLECTIONS_DIR)
|
||||||
const collection = parseCollectionYaml(filePath);
|
.filter((file) => file.endsWith(".collection.yml"));
|
||||||
|
|
||||||
if (collection) {
|
for (const file of collectionFiles) {
|
||||||
const collectionId =
|
const filePath = path.join(COLLECTIONS_DIR, file);
|
||||||
collection.id || path.basename(file, ".collection.yml");
|
const collection = parseCollectionYaml(filePath);
|
||||||
const readmeContent = generateCollectionReadme(
|
|
||||||
collection,
|
if (collection) {
|
||||||
collectionId,
|
const collectionId =
|
||||||
registryNames
|
collection.id || path.basename(file, ".collection.yml");
|
||||||
);
|
const readmeContent = generateCollectionReadme(
|
||||||
const readmeFile = path.join(COLLECTIONS_DIR, `${collectionId}.md`);
|
collection,
|
||||||
writeFileIfChanged(readmeFile, readmeContent);
|
collectionId,
|
||||||
|
registryNames
|
||||||
|
);
|
||||||
|
const readmeFile = path.join(COLLECTIONS_DIR, `${collectionId}.md`);
|
||||||
|
writeFileIfChanged(readmeFile, readmeContent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Generate featured collections section and update main README.md
|
// Generate featured collections section and update main README.md
|
||||||
console.log("Updating main README.md with featured collections...");
|
console.log("Updating main README.md with featured collections...");
|
||||||
const featuredSection = generateFeaturedCollectionsSection(COLLECTIONS_DIR);
|
const featuredSection = generateFeaturedCollectionsSection(COLLECTIONS_DIR);
|
||||||
|
|
||||||
if (featuredSection) {
|
if (featuredSection) {
|
||||||
const mainReadmePath = path.join(ROOT_FOLDER, "README.md");
|
const mainReadmePath = path.join(ROOT_FOLDER, "README.md");
|
||||||
|
|
||||||
if (fs.existsSync(mainReadmePath)) {
|
if (fs.existsSync(mainReadmePath)) {
|
||||||
let readmeContent = fs.readFileSync(mainReadmePath, "utf8");
|
let readmeContent = fs.readFileSync(mainReadmePath, "utf8");
|
||||||
|
|
||||||
// Define markers to identify where to insert the featured collections
|
// Define markers to identify where to insert the featured collections
|
||||||
const startMarker = "## 🌟 Featured Collections";
|
const startMarker = "## 🌟 Featured Collections";
|
||||||
const endMarker = "## MCP Server";
|
const endMarker = "## MCP Server";
|
||||||
|
|
||||||
// Check if the section already exists
|
// Check if the section already exists
|
||||||
const startIndex = readmeContent.indexOf(startMarker);
|
const startIndex = readmeContent.indexOf(startMarker);
|
||||||
|
|
||||||
if (startIndex !== -1) {
|
if (startIndex !== -1) {
|
||||||
// Section exists, replace it
|
// Section exists, replace it
|
||||||
const endIndex = readmeContent.indexOf(endMarker, startIndex);
|
const endIndex = readmeContent.indexOf(endMarker, startIndex);
|
||||||
if (endIndex !== -1) {
|
if (endIndex !== -1) {
|
||||||
// Replace the existing section
|
// Replace the existing section
|
||||||
const beforeSection = readmeContent.substring(0, startIndex);
|
const beforeSection = readmeContent.substring(0, startIndex);
|
||||||
const afterSection = readmeContent.substring(endIndex);
|
const afterSection = readmeContent.substring(endIndex);
|
||||||
readmeContent =
|
readmeContent =
|
||||||
beforeSection + featuredSection + "\n\n" + afterSection;
|
beforeSection + featuredSection + "\n\n" + afterSection;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Section doesn't exist, insert it before "## MCP Server"
|
||||||
|
const mcpIndex = readmeContent.indexOf(endMarker);
|
||||||
|
if (mcpIndex !== -1) {
|
||||||
|
const beforeMcp = readmeContent.substring(0, mcpIndex);
|
||||||
|
const afterMcp = readmeContent.substring(mcpIndex);
|
||||||
|
readmeContent = beforeMcp + featuredSection + "\n\n" + afterMcp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeFileIfChanged(mainReadmePath, readmeContent);
|
||||||
|
console.log("Main README.md updated with featured collections");
|
||||||
} else {
|
} else {
|
||||||
// Section doesn't exist, insert it before "## MCP Server"
|
console.warn(
|
||||||
const mcpIndex = readmeContent.indexOf(endMarker);
|
"README.md not found, skipping featured collections update"
|
||||||
if (mcpIndex !== -1) {
|
);
|
||||||
const beforeMcp = readmeContent.substring(0, mcpIndex);
|
|
||||||
const afterMcp = readmeContent.substring(mcpIndex);
|
|
||||||
readmeContent = beforeMcp + featuredSection + "\n\n" + afterMcp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFileIfChanged(mainReadmePath, readmeContent);
|
|
||||||
console.log("Main README.md updated with featured collections");
|
|
||||||
} else {
|
} else {
|
||||||
console.warn("README.md not found, skipping featured collections update");
|
console.log("No featured collections found to add to README.md");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log("No featured collections found to add to README.md");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error generating category README files: ${error.message}`);
|
console.error(`Error generating category README files: ${error.message}`);
|
||||||
console.error(error.stack);
|
console.error(error.stack);
|
||||||
@@ -1024,4 +1118,3 @@ main().catch((error) => {
|
|||||||
console.error(error.stack);
|
console.error(error.stack);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
172
eng/validate-skills.mjs
Normal file
172
eng/validate-skills.mjs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { parseSkillMetadata } from "./yaml-parser.mjs";
|
||||||
|
import {
|
||||||
|
ROOT_FOLDER,
|
||||||
|
SKILLS_DIR,
|
||||||
|
SKILL_NAME_MIN_LENGTH,
|
||||||
|
SKILL_NAME_MAX_LENGTH,
|
||||||
|
SKILL_DESCRIPTION_MIN_LENGTH,
|
||||||
|
SKILL_DESCRIPTION_MAX_LENGTH,
|
||||||
|
} from "./constants.mjs";
|
||||||
|
|
||||||
|
// Validation functions
|
||||||
|
function validateSkillName(name) {
|
||||||
|
if (!name || typeof name !== "string") {
|
||||||
|
return "name is required and must be a string";
|
||||||
|
}
|
||||||
|
if (!/^[a-z0-9-]+$/.test(name)) {
|
||||||
|
return "name must contain only lowercase letters, numbers, and hyphens";
|
||||||
|
}
|
||||||
|
if (name.length < SKILL_NAME_MIN_LENGTH || name.length > SKILL_NAME_MAX_LENGTH) {
|
||||||
|
return `name must be between ${SKILL_NAME_MIN_LENGTH} and ${SKILL_NAME_MAX_LENGTH} characters`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSkillDescription(description) {
|
||||||
|
if (!description || typeof description !== "string") {
|
||||||
|
return "description is required and must be a string";
|
||||||
|
}
|
||||||
|
if (description.length < SKILL_DESCRIPTION_MIN_LENGTH) {
|
||||||
|
return `description must be at least ${SKILL_DESCRIPTION_MIN_LENGTH} characters`;
|
||||||
|
}
|
||||||
|
if (description.length > SKILL_DESCRIPTION_MAX_LENGTH) {
|
||||||
|
return `description must not exceed ${SKILL_DESCRIPTION_MAX_LENGTH} characters`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSkillFolder(folderPath, folderName) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
// Check if SKILL.md exists
|
||||||
|
const skillFile = path.join(folderPath, "SKILL.md");
|
||||||
|
if (!fs.existsSync(skillFile)) {
|
||||||
|
errors.push("Missing SKILL.md file");
|
||||||
|
return errors; // Cannot proceed without SKILL.md
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and validate frontmatter
|
||||||
|
const metadata = parseSkillMetadata(folderPath);
|
||||||
|
if (!metadata) {
|
||||||
|
errors.push("Failed to parse SKILL.md frontmatter");
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate name field
|
||||||
|
const nameError = validateSkillName(metadata.name);
|
||||||
|
if (nameError) {
|
||||||
|
errors.push(`name: ${nameError}`);
|
||||||
|
} else {
|
||||||
|
// Validate that folder name matches skill name
|
||||||
|
if (metadata.name !== folderName) {
|
||||||
|
errors.push(
|
||||||
|
`Folder name "${folderName}" does not match skill name "${metadata.name}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate description field
|
||||||
|
const descError = validateSkillDescription(metadata.description);
|
||||||
|
if (descError) {
|
||||||
|
errors.push(`description: ${descError}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for reasonable file sizes in bundled assets
|
||||||
|
const MAX_ASSET_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||||
|
for (const asset of metadata.assets) {
|
||||||
|
const assetPath = path.join(folderPath, asset);
|
||||||
|
try {
|
||||||
|
const stats = fs.statSync(assetPath);
|
||||||
|
if (stats.size > MAX_ASSET_SIZE) {
|
||||||
|
errors.push(
|
||||||
|
`Bundled asset "${asset}" exceeds maximum size of 5MB (${(
|
||||||
|
stats.size /
|
||||||
|
1024 /
|
||||||
|
1024
|
||||||
|
).toFixed(2)}MB)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`Cannot access bundled asset "${asset}": ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main validation function
|
||||||
|
function validateSkills() {
|
||||||
|
if (!fs.existsSync(SKILLS_DIR)) {
|
||||||
|
console.log("No skills directory found - validation skipped");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillFolders = fs
|
||||||
|
.readdirSync(SKILLS_DIR)
|
||||||
|
.filter((file) => {
|
||||||
|
const filePath = path.join(SKILLS_DIR, file);
|
||||||
|
return fs.statSync(filePath).isDirectory();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (skillFolders.length === 0) {
|
||||||
|
console.log("No skill folders found - validation skipped");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Validating ${skillFolders.length} skill folder(s)...`);
|
||||||
|
|
||||||
|
let hasErrors = false;
|
||||||
|
const usedNames = new Set();
|
||||||
|
|
||||||
|
for (const folder of skillFolders) {
|
||||||
|
const folderPath = path.join(SKILLS_DIR, folder);
|
||||||
|
console.log(`\nValidating ${folder}...`);
|
||||||
|
|
||||||
|
const errors = validateSkillFolder(folderPath, folder);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.error(`❌ Validation errors in ${folder}:`);
|
||||||
|
errors.forEach((error) => console.error(` - ${error}`));
|
||||||
|
hasErrors = true;
|
||||||
|
} else {
|
||||||
|
console.log(`✅ ${folder} is valid`);
|
||||||
|
|
||||||
|
// Check for duplicate names (only if no errors)
|
||||||
|
const metadata = parseSkillMetadata(folderPath);
|
||||||
|
if (metadata) {
|
||||||
|
if (usedNames.has(metadata.name)) {
|
||||||
|
console.error(
|
||||||
|
`❌ Duplicate skill name "${metadata.name}" found in ${folder}`
|
||||||
|
);
|
||||||
|
hasErrors = true;
|
||||||
|
} else {
|
||||||
|
usedNames.add(metadata.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasErrors) {
|
||||||
|
console.log(`\n✅ All ${skillFolders.length} skills are valid`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !hasErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run validation
|
||||||
|
try {
|
||||||
|
const isValid = validateSkills();
|
||||||
|
if (!isValid) {
|
||||||
|
console.error("\n❌ Skill validation failed");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log("\n🎉 Skill validation passed");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error during validation: ${error.message}`);
|
||||||
|
console.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// YAML parser for collection files and frontmatter parsing using vfile-matter
|
// YAML parser for collection files and frontmatter parsing using vfile-matter
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { VFile } from "vfile";
|
import { VFile } from "vfile";
|
||||||
import { matter } from "vfile-matter";
|
import { matter } from "vfile-matter";
|
||||||
@@ -137,11 +138,56 @@ function extractMcpServerConfigs(filePath) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse SKILL.md frontmatter and list bundled assets in a skill folder
|
||||||
|
* @param {string} skillPath - Path to skill folder
|
||||||
|
* @returns {object|null} Skill metadata with name, description, and assets array
|
||||||
|
*/
|
||||||
|
function parseSkillMetadata(skillPath) {
|
||||||
|
return safeFileOperation(
|
||||||
|
() => {
|
||||||
|
const skillFile = path.join(skillPath, "SKILL.md");
|
||||||
|
if (!fs.existsSync(skillFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontmatter = parseFrontmatter(skillFile);
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!frontmatter?.name || !frontmatter?.description) {
|
||||||
|
console.warn(
|
||||||
|
`Invalid skill at ${skillPath}: missing name or description in frontmatter`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List bundled assets (all files except SKILL.md)
|
||||||
|
const assets = fs
|
||||||
|
.readdirSync(skillPath)
|
||||||
|
.filter((file) => {
|
||||||
|
const filePath = path.join(skillPath, file);
|
||||||
|
return file !== "SKILL.md" && fs.statSync(filePath).isFile();
|
||||||
|
})
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: frontmatter.name,
|
||||||
|
description: frontmatter.description,
|
||||||
|
assets,
|
||||||
|
path: skillPath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
skillPath,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
parseCollectionYaml,
|
parseCollectionYaml,
|
||||||
parseFrontmatter,
|
parseFrontmatter,
|
||||||
extractAgentMetadata,
|
extractAgentMetadata,
|
||||||
extractMcpServers,
|
extractMcpServers,
|
||||||
extractMcpServerConfigs,
|
extractMcpServerConfigs,
|
||||||
|
parseSkillMetadata,
|
||||||
safeFileOperation,
|
safeFileOperation,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
"contributors:generate": "all-contributors generate",
|
"contributors:generate": "all-contributors generate",
|
||||||
"contributors:check": "all-contributors check",
|
"contributors:check": "all-contributors check",
|
||||||
"collection:validate": "node ./eng/validate-collections.mjs",
|
"collection:validate": "node ./eng/validate-collections.mjs",
|
||||||
"collection:create": "node ./eng/create-collection.mjs"
|
"collection:create": "node ./eng/create-collection.mjs",
|
||||||
|
"skill:validate": "node ./eng/validate-skills.mjs",
|
||||||
|
"skill:create": "node ./eng/create-skill.mjs"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
116
skills/webapp-testing/SKILL.md
Normal file
116
skills/webapp-testing/SKILL.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
---
|
||||||
|
name: webapp-testing
|
||||||
|
description: Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Web Application Testing
|
||||||
|
|
||||||
|
This skill enables comprehensive testing and debugging of local web applications using Playwright automation.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Use this skill when you need to:
|
||||||
|
- Test frontend functionality in a real browser
|
||||||
|
- Verify UI behavior and interactions
|
||||||
|
- Debug web application issues
|
||||||
|
- Capture screenshots for documentation or debugging
|
||||||
|
- Inspect browser console logs
|
||||||
|
- Validate form submissions and user flows
|
||||||
|
- Check responsive design across viewports
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js installed on the system
|
||||||
|
- A locally running web application (or accessible URL)
|
||||||
|
- Playwright will be installed automatically if not present
|
||||||
|
|
||||||
|
## Core Capabilities
|
||||||
|
|
||||||
|
### 1. Browser Automation
|
||||||
|
- Navigate to URLs
|
||||||
|
- Click buttons and links
|
||||||
|
- Fill form fields
|
||||||
|
- Select dropdowns
|
||||||
|
- Handle dialogs and alerts
|
||||||
|
|
||||||
|
### 2. Verification
|
||||||
|
- Assert element presence
|
||||||
|
- Verify text content
|
||||||
|
- Check element visibility
|
||||||
|
- Validate URLs
|
||||||
|
- Test responsive behavior
|
||||||
|
|
||||||
|
### 3. Debugging
|
||||||
|
- Capture screenshots
|
||||||
|
- View console logs
|
||||||
|
- Inspect network requests
|
||||||
|
- Debug failed tests
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Example 1: Basic Navigation Test
|
||||||
|
```javascript
|
||||||
|
// Navigate to a page and verify title
|
||||||
|
await page.goto('http://localhost:3000');
|
||||||
|
const title = await page.title();
|
||||||
|
console.log('Page title:', title);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Form Interaction
|
||||||
|
```javascript
|
||||||
|
// Fill out and submit a form
|
||||||
|
await page.fill('#username', 'testuser');
|
||||||
|
await page.fill('#password', 'password123');
|
||||||
|
await page.click('button[type="submit"]');
|
||||||
|
await page.waitForURL('**/dashboard');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Screenshot Capture
|
||||||
|
```javascript
|
||||||
|
// Capture a screenshot for debugging
|
||||||
|
await page.screenshot({ path: 'debug.png', fullPage: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
1. **Always verify the app is running** - Check that the local server is accessible before running tests
|
||||||
|
2. **Use explicit waits** - Wait for elements or navigation to complete before interacting
|
||||||
|
3. **Capture screenshots on failure** - Take screenshots to help debug issues
|
||||||
|
4. **Clean up resources** - Always close the browser when done
|
||||||
|
5. **Handle timeouts gracefully** - Set reasonable timeouts for slow operations
|
||||||
|
6. **Test incrementally** - Start with simple interactions before complex flows
|
||||||
|
7. **Use selectors wisely** - Prefer data-testid or role-based selectors over CSS classes
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern: Wait for Element
|
||||||
|
```javascript
|
||||||
|
await page.waitForSelector('#element-id', { state: 'visible' });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Check if Element Exists
|
||||||
|
```javascript
|
||||||
|
const exists = await page.locator('#element-id').count() > 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Get Console Logs
|
||||||
|
```javascript
|
||||||
|
page.on('console', msg => console.log('Browser log:', msg.text()));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Handle Errors
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
await page.click('#button');
|
||||||
|
} catch (error) {
|
||||||
|
await page.screenshot({ path: 'error.png' });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Requires Node.js environment
|
||||||
|
- Cannot test native mobile apps (use React Native Testing Library instead)
|
||||||
|
- May have issues with complex authentication flows
|
||||||
|
- Some modern frameworks may require specific configuration
|
||||||
56
skills/webapp-testing/test-helper.js
Normal file
56
skills/webapp-testing/test-helper.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Helper utilities for web application testing with Playwright
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a condition to be true with timeout
|
||||||
|
* @param {Function} condition - Function that returns boolean
|
||||||
|
* @param {number} timeout - Timeout in milliseconds
|
||||||
|
* @param {number} interval - Check interval in milliseconds
|
||||||
|
*/
|
||||||
|
async function waitForCondition(condition, timeout = 5000, interval = 100) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
while (Date.now() - startTime < timeout) {
|
||||||
|
if (await condition()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, interval));
|
||||||
|
}
|
||||||
|
throw new Error('Condition not met within timeout');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture browser console logs
|
||||||
|
* @param {Page} page - Playwright page object
|
||||||
|
* @returns {Array} Array of console messages
|
||||||
|
*/
|
||||||
|
function captureConsoleLogs(page) {
|
||||||
|
const logs = [];
|
||||||
|
page.on('console', msg => {
|
||||||
|
logs.push({
|
||||||
|
type: msg.type(),
|
||||||
|
text: msg.text(),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take screenshot with automatic naming
|
||||||
|
* @param {Page} page - Playwright page object
|
||||||
|
* @param {string} name - Base name for screenshot
|
||||||
|
*/
|
||||||
|
async function captureScreenshot(page, name) {
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const filename = `${name}-${timestamp}.png`;
|
||||||
|
await page.screenshot({ path: filename, fullPage: true });
|
||||||
|
console.log(`Screenshot saved: ${filename}`);
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
waitForCondition,
|
||||||
|
captureConsoleLogs,
|
||||||
|
captureScreenshot
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user