Add metadata.json and its extraction script

This commit is contained in:
Justin Yoo 2025-07-17 15:41:14 +09:00
parent db0d47413a
commit 9b8b80b0ec
5 changed files with 2090 additions and 0 deletions

View File

@ -1,5 +1,7 @@
When performing a code review, validate that there are changes in the `README.md` file that match the changes in the pull request. If there are no changes, or if the changes do not match, then the pull request is not ready to be merged.
When performing a code review, validate that there are changes in the `metadata.json` file that match the changes in the pull request. If there are no changes, or if the changes do not match, then the pull request is not ready to be merged.
When performing a code review, ensure that the values in the front matter are wrapped in single quotes.
When performing a code review, ensure that the `description` field in the front matter is not empty.

79
.github/workflows/validate-metadata.yml vendored Normal file
View File

@ -0,0 +1,79 @@
name: Validate metadata.json
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "instructions/**"
- "prompts/**"
- "chatmodes/**"
- "*.js"
jobs:
validate-metadata:
permissions:
pull-requests: write
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Update metadata.json
run: node update-metadata.js
- name: Check for metadata.json changes
id: check-diff
run: |
if git diff --exit-code metadata.json; then
echo "No changes to metadata.json after running update script."
echo "status=success" >> $GITHUB_OUTPUT
else
echo "Changes detected in metadata.json after running update script."
echo "status=failure" >> $GITHUB_OUTPUT
echo "diff<<EOF" >> $GITHUB_OUTPUT
git diff metadata.json >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
fi
- name: Output diff to logs for non-write users
if: steps.check-diff.outputs.status == 'failure' && github.event.pull_request.head.repo.permissions.push != true
run: |
echo "::group::metadata.json diff (changes needed)"
echo "The following changes need to be made to metadata.json:"
echo ""
git diff metadata.json
echo "::endgroup::"
- name: Comment on PR if metadata.json needs updating
if: steps.check-diff.outputs.status == 'failure' && github.event.pull_request.head.repo.permissions.push == true
uses: marocchino/sticky-pull-request-comment@v2
with:
header: metadata-validation
message: |
## ⚠️ `metadata.json` needs to be updated
The `update-metadata.js` script detected changes that need to be made to the `metadata.json` file.
Please run `node update-metadata.js` locally and commit the changes before merging this PR.
<details>
<summary>View diff</summary>
```diff
${{ steps.check-diff.outputs.diff }}
```
</details>
- name: Fail workflow if metadata.json needs updating
if: steps.check-diff.outputs.status == 'failure'
run: |
echo "❌ `metadata.json` needs to be updated. Please run 'node update-metadata.js' locally and commit the changes."
exit 1

121
frontmatter-schema.json Normal file
View File

@ -0,0 +1,121 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Frontmatter Schema",
"description": "Schema for validating frontmatter data in the awesome-copilot repository",
"type": "object",
"properties": {
"chatmodes": {
"type": "array",
"description": "Array of chat mode configurations",
"items": {
"$ref": "#/definitions/chatmode"
}
},
"instructions": {
"type": "array",
"description": "Array of instruction file configurations",
"items": {
"$ref": "#/definitions/instruction"
}
},
"prompts": {
"type": "array",
"description": "Array of prompt file configurations",
"items": {
"$ref": "#/definitions/prompt"
}
}
},
"additionalProperties": false,
"definitions": {
"chatmode": {
"type": "object",
"description": "Configuration for a chat mode",
"properties": {
"filename": {
"type": "string",
"description": "Name of the chat mode file",
"pattern": "^[a-zA-Z0-9._-]+\\.chatmode\\.md$"
},
"title": {
"type": "string",
"description": "Display title for the chat mode",
"minLength": 1
},
"description": {
"type": "string",
"description": "Description of the chat mode functionality",
"minLength": 1
},
"model": {
"type": "string",
"description": "AI model to use for this chat mode"
},
"tools": {
"type": "array",
"description": "Array of available tools for this chat mode",
"items": {
"type": "string"
}
}
},
"required": ["filename", "description"],
"additionalProperties": false
},
"instruction": {
"type": "object",
"description": "Configuration for an instruction file",
"properties": {
"filename": {
"type": "string",
"description": "Name of the instruction file",
"pattern": "^[a-zA-Z0-9._-]+\\.instructions\\.md$"
},
"description": {
"type": "string",
"description": "Description of the instruction file purpose",
"minLength": 1
},
"applyTo": {
"type": "array",
"description": "File patterns that this instruction applies to",
"items": {
"type": "string",
"minLength": 1
}
}
},
"required": ["filename", "description"],
"additionalProperties": false
},
"prompt": {
"type": "object",
"description": "Configuration for a prompt file",
"properties": {
"filename": {
"type": "string",
"description": "Name of the prompt file",
"pattern": "^[a-zA-Z0-9._-]+\\.prompt\\.md$"
},
"description": {
"type": "string",
"description": "Description of the prompt functionality",
"minLength": 1
},
"mode": {
"type": "string",
"description": "Execution mode for the prompt"
},
"tools": {
"type": "array",
"description": "Array of available tools for this prompt",
"items": {
"type": "string"
}
}
},
"required": ["filename", "description"],
"additionalProperties": false
}
}
}

1647
metadata.json Normal file

File diff suppressed because it is too large Load Diff

241
update-metadata.js Normal file
View File

@ -0,0 +1,241 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
// Read the JSON schema to understand the structure
const schemaPath = path.join(__dirname, 'frontmatter-schema.json');
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
// Define the directories to process
const directories = {
chatmodes: path.join(__dirname, 'chatmodes'),
instructions: path.join(__dirname, 'instructions'),
prompts: path.join(__dirname, 'prompts')
};
// Simple YAML parser for frontmatter
function parseSimpleYaml(yamlContent) {
const result = {};
const lines = yamlContent.split('\n');
let currentKey = null;
let currentValue = '';
let inArray = false;
let arrayItems = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
// Check if this is a key-value pair
const colonIndex = trimmed.indexOf(':');
if (colonIndex !== -1 && !trimmed.startsWith('-')) {
// Finish previous key if we were building one
if (currentKey) {
if (inArray) {
result[currentKey] = arrayItems;
arrayItems = [];
inArray = false;
} else {
result[currentKey] = currentValue.trim();
}
}
currentKey = trimmed.substring(0, colonIndex).trim();
currentValue = trimmed.substring(colonIndex + 1).trim();
// Remove quotes if present
if ((currentValue.startsWith('"') && currentValue.endsWith('"')) ||
(currentValue.startsWith("'") && currentValue.endsWith("'"))) {
currentValue = currentValue.slice(1, -1);
}
// Check if this is an array
if (currentValue.startsWith('[') && currentValue.endsWith(']')) {
const arrayContent = currentValue.slice(1, -1);
if (arrayContent.trim()) {
result[currentKey] = arrayContent.split(',').map(item => {
item = item.trim();
// Remove quotes from array items
if ((item.startsWith('"') && item.endsWith('"')) ||
(item.startsWith("'") && item.endsWith("'"))) {
item = item.slice(1, -1);
}
return item;
});
} else {
result[currentKey] = [];
}
currentKey = null;
currentValue = '';
} else if (currentValue === '' || currentValue === '[]') {
// Empty value or empty array, might be multi-line
if (currentValue === '[]') {
result[currentKey] = [];
currentKey = null;
currentValue = '';
} else {
// Check if next line starts with a dash (array item)
if (i + 1 < lines.length && lines[i + 1].trim().startsWith('-')) {
inArray = true;
arrayItems = [];
}
}
}
} else if (trimmed.startsWith('-') && currentKey && inArray) {
// Array item
let item = trimmed.substring(1).trim();
// Remove quotes
if ((item.startsWith('"') && item.endsWith('"')) ||
(item.startsWith("'") && item.endsWith("'"))) {
item = item.slice(1, -1);
}
arrayItems.push(item);
} else if (currentKey && !inArray) {
// Multi-line value
currentValue += ' ' + trimmed;
}
}
// Finish the last key
if (currentKey) {
if (inArray) {
result[currentKey] = arrayItems;
} else {
let finalValue = currentValue.trim();
// Remove quotes if present
if ((finalValue.startsWith('"') && finalValue.endsWith('"')) ||
(finalValue.startsWith("'") && finalValue.endsWith("'"))) {
finalValue = finalValue.slice(1, -1);
}
result[currentKey] = finalValue;
}
}
return result;
}
// Function to extract frontmatter from a markdown file
function extractFrontmatter(filePath) {
let content = fs.readFileSync(filePath, 'utf8');
// Remove BOM if present (handles files with Byte Order Mark)
if (content.charCodeAt(0) === 0xFEFF) {
content = content.slice(1);
}
// Check if the file starts with frontmatter
if (!content.startsWith('---')) {
return null;
}
const lines = content.split('\n');
let frontmatterEnd = -1;
// Find the end of frontmatter
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim() === '---') {
frontmatterEnd = i;
break;
}
}
if (frontmatterEnd === -1) {
return null;
}
// Extract frontmatter content
const frontmatterContent = lines.slice(1, frontmatterEnd).join('\n');
try {
return parseSimpleYaml(frontmatterContent);
} catch (error) {
console.error(`Error parsing frontmatter in ${filePath}:`, error.message);
return null;
}
}
// Function to process files in a directory
function processDirectory(dirPath, fileExtension) {
const files = fs.readdirSync(dirPath)
.filter(file => file.endsWith(fileExtension))
.sort();
const results = [];
for (const file of files) {
const filePath = path.join(dirPath, file);
const frontmatter = extractFrontmatter(filePath);
if (frontmatter) {
const result = {
filename: file,
...frontmatter
};
// Ensure description is present (required by schema)
if (!result.description) {
console.warn(`Warning: No description found in ${file}, adding placeholder`);
result.description = 'No description provided';
}
results.push(result);
} else {
console.warn(`Warning: No frontmatter found in ${file}, skipping`);
}
}
return results;
}
// Process all directories
const metadata = {
chatmodes: processDirectory(directories.chatmodes, '.chatmode.md'),
instructions: processDirectory(directories.instructions, '.instructions.md'),
prompts: processDirectory(directories.prompts, '.prompt.md')
};
// Write the metadata.json file
const outputPath = path.join(__dirname, 'metadata.json');
fs.writeFileSync(outputPath, JSON.stringify(metadata, null, 2));
console.log(`Extracted frontmatter from ${metadata.chatmodes.length} chatmode files`);
console.log(`Extracted frontmatter from ${metadata.instructions.length} instruction files`);
console.log(`Extracted frontmatter from ${metadata.prompts.length} prompt files`);
console.log(`Metadata written to ${outputPath}`);
// Validate that required fields are present
let hasErrors = false;
// Check chatmodes
metadata.chatmodes.forEach(chatmode => {
if (!chatmode.filename || !chatmode.description) {
console.error(`Error: Chatmode missing required fields: ${chatmode.filename || 'unknown'}`);
hasErrors = true;
}
});
// Check instructions
metadata.instructions.forEach(instruction => {
if (!instruction.filename || !instruction.description) {
console.error(`Error: Instruction missing required fields: ${instruction.filename || 'unknown'}`);
hasErrors = true;
}
});
// Check prompts
metadata.prompts.forEach(prompt => {
if (!prompt.filename || !prompt.description) {
console.error(`Error: Prompt missing required fields: ${prompt.filename || 'unknown'}`);
hasErrors = true;
}
});
if (hasErrors) {
console.error('Some files are missing required fields. Please check the output above.');
process.exit(1);
} else {
console.log('All files have required fields. Metadata extraction completed successfully.');
}